URI: 
       tMerge pull request #5993 from TheCharlatan/bitbox02New - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 3745f35f69ff9d2add6b94ce27cfca12baa65e47
   DIR parent bfffc7cb1ec3372d771371b5731226d1a5c8bafa
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Sun, 12 Apr 2020 13:49:35 +0000
       
       Merge pull request #5993 from TheCharlatan/bitbox02New
       
       BitBox02 Electrum plugin support
       Diffstat:
         M contrib/build-wine/deterministic.s… |       2 ++
         M contrib/deterministic-build/requir… |      63 +++++++++++++++++++++++++++++++
         M contrib/osx/osx.spec                |       2 ++
         M contrib/requirements/requirements-… |       1 +
         A contrib/udev/53-hid-bitbox02.rules  |       1 +
         A contrib/udev/54-hid-bitbox02.rules  |       1 +
         M contrib/udev/README.md              |       3 ++-
         M electrum/bitcoin.py                 |      37 ++++++++++++++++++++++++++++++-
         A electrum/gui/icons/bitbox02.png     |       0 
         A electrum/gui/icons/bitbox02_unpair… |       0 
         M electrum/gui/qt/main_window.py      |      10 ++++++++--
         M electrum/gui/qt/util.py             |       2 ++
         M electrum/plugin.py                  |      27 ++++++++-------------------
         A electrum/plugins/bitbox02/__init__… |      14 ++++++++++++++
         A electrum/plugins/bitbox02/bitbox02… |     617 +++++++++++++++++++++++++++++++
         A electrum/plugins/bitbox02/qt.py     |     127 +++++++++++++++++++++++++++++++
         M electrum/plugins/coldcard/cmdline.… |       6 ------
         M electrum/plugins/coldcard/coldcard… |       2 +-
         M electrum/plugins/coldcard/qt.py     |      18 ++----------------
         M electrum/plugins/digitalbitbox/dig… |       2 +-
         M electrum/plugins/hw_wallet/plugin.… |      18 +++++++++++++++++-
         M electrum/plugins/keepkey/keepkey.py |       2 +-
         M electrum/plugins/ledger/ledger.py   |       2 +-
       
       23 files changed, 907 insertions(+), 50 deletions(-)
       ---
   DIR diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
       t@@ -23,6 +23,7 @@ hiddenimports += collect_submodules('btchip')
        hiddenimports += collect_submodules('keepkeylib')
        hiddenimports += collect_submodules('websocket')
        hiddenimports += collect_submodules('ckcc')
       +hiddenimports += collect_submodules('bitbox02')
        hiddenimports += ['PyQt5.QtPrintSupport']  # needed by Revealer
        
        
       t@@ -48,6 +49,7 @@ datas += collect_data_files('safetlib')
        datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
        datas += collect_data_files('ckcc')
       +datas += collect_data_files('bitbox02')
        datas += collect_data_files('jsonrpcserver')
        datas += collect_data_files('jsonrpcclient')
        
   DIR diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt
       t@@ -1,8 +1,43 @@
       +base58==2.0.0 \
       +    --hash=sha256:4c7f5687da771b519cf86b3236250e7c3543368c576404c9fe2d992a287666e0 \
       +    --hash=sha256:c83584a8b917dc52dd634307137f2ad2721a9efb4f1de32fc7eaaaf87844177e
       +bitbox02==2.0.3 \
       +    --hash=sha256:1f0164fd9941d3c3a17fb7db3bceddd89458986ef3da6171845e6433c3f66889 \
       +    --hash=sha256:53d06baafc597a8d14f990e285cd608cdf00be41a6d42ae40c316abad7798bd5
        btchip-python==0.1.28 \
            --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
        certifi==2020.4.5.1 \
            --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \
            --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519
       +cffi==1.14.0 \
       +    --hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \
       +    --hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \
       +    --hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \
       +    --hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \
       +    --hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \
       +    --hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \
       +    --hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6 \
       +    --hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \
       +    --hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \
       +    --hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \
       +    --hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \
       +    --hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \
       +    --hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \
       +    --hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \
       +    --hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \
       +    --hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \
       +    --hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \
       +    --hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \
       +    --hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \
       +    --hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \
       +    --hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \
       +    --hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \
       +    --hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \
       +    --hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \
       +    --hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \
       +    --hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \
       +    --hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \
       +    --hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c
        chardet==3.0.4 \
            --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
            --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
       t@@ -14,6 +49,26 @@ click==7.1.1 \
            --hash=sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a
        construct==2.10.56 \
            --hash=sha256:97ba13edcd98546f10f7555af41c8ce7ae9d8221525ec4062c03f9adbf940661
       +cryptography==2.9 \
       +    --hash=sha256:0cacd3ef5c604b8e5f59bf2582c076c98a37fe206b31430d0cd08138aff0986e \
       +    --hash=sha256:192ca04a36852a994ef21df13cca4d822adbbdc9d5009c0f96f1d2929e375d4f \
       +    --hash=sha256:19ae795137682a9778892fb4390c07811828b173741bce91e30f899424b3934d \
       +    --hash=sha256:1b9b535d6b55936a79dbe4990b64bb16048f48747c76c29713fea8c50eca2acf \
       +    --hash=sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02 \
       +    --hash=sha256:3be7a5722d5bfe69894d3f7bbed15547b17619f3a88a318aab2e37f457524164 \
       +    --hash=sha256:49870684da168b90110bbaf86140d4681032c5e6a2461adc7afdd93be5634216 \
       +    --hash=sha256:587f98ce27ac4547177a0c6fe0986b8736058daffe9160dcf5f1bd411b7fbaa1 \
       +    --hash=sha256:5aca6f00b2f42546b9bdf11a69f248d1881212ce5b9e2618b04935b87f6f82a1 \
       +    --hash=sha256:6b744039b55988519cc183149cceb573189b3e46e16ccf6f8c46798bb767c9dc \
       +    --hash=sha256:6b91cab3841b4c7cb70e4db1697c69f036c8bc0a253edc0baa6783154f1301e4 \
       +    --hash=sha256:7598974f6879a338c785c513e7c5a4329fbc58b9f6b9a6305035fca5b1076552 \
       +    --hash=sha256:7a279f33a081d436e90e91d1a7c338553c04e464de1c9302311a5e7e4b746088 \
       +    --hash=sha256:95e1296e0157361fe2f5f0ed307fd31f94b0ca13372e3673fa95095a627636a1 \
       +    --hash=sha256:9fc9da390e98cb6975eadf251b6e5fa088820141061bf041cd5c72deba1dc526 \
       +    --hash=sha256:cc20316e3f5a6b582fc3b029d8dc03aabeb645acfcb7fc1d9848841a33265748 \
       +    --hash=sha256:d1bf5a1a0d60c7f9a78e448adcb99aa101f3f9588b16708044638881be15d6bc \
       +    --hash=sha256:ed1d0760c7e46436ec90834d6f10477ff09475c692ed1695329d324b2c5cd547 \
       +    --hash=sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585
        Cython==0.29.16 \
            --hash=sha256:0542a6c4ff1be839b6479deffdbdff1a330697d7953dd63b6de99c078e3acd5f \
            --hash=sha256:0bcf7f87aa0ba8b62d4f3b6e0146e48779eaa4f39f92092d7ff90081ef6133e0 \
       t@@ -72,6 +127,8 @@ libusb1==1.7.1 \
        mnemonic==0.19 \
            --hash=sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931 \
            --hash=sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6
       +noiseprotocol==0.3.1 \
       +    --hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111
        pip==20.0.2 \
            --hash=sha256:4ae14a42d8adba3205ebeb38aa68cfc0b6c346e1ae2e699a0b3bad4da19cef5c \
            --hash=sha256:7db0c8ea4c7ea51c8049640e8e6e7fde949de672bfa4949920675563a5a6967f
       t@@ -97,12 +154,18 @@ protobuf==3.11.3 \
            --hash=sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80
        pyaes==1.6.1 \
            --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
       +pycparser==2.20 \
       +    --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
       +    --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
        requests==2.23.0 \
            --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \
            --hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6
        safet==0.1.5 \
            --hash=sha256:a7fd4b68bb1bc6185298af665c8e8e00e2bb2bcbddbb22844ead929b845c635e \
            --hash=sha256:f966a23243312f64d14c7dfe02e8f13f6eeba4c3f51341f2c11ae57831f07de3
       +semver==2.9.1 \
       +    --hash=sha256:095c3cba6d5433f21451101463b22cf831fe6996fcc8a603407fd8bea54f116b \
       +    --hash=sha256:723be40c74b6468861e0e3dbb80a41fc3b171a2a45bf956c245304773dc06055
        setuptools==46.1.3 \
            --hash=sha256:4fe404eec2738c20ab5841fa2d791902d2a645f32318a7850ef26f8d7215a8ee \
            --hash=sha256:795e0475ba6cd7fa082b1ee6e90d552209995627a2a227a47c6ea93282f4bfb1
   DIR diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec
       t@@ -66,6 +66,7 @@ hiddenimports += collect_submodules('btchip')
        hiddenimports += collect_submodules('keepkeylib')
        hiddenimports += collect_submodules('websocket')
        hiddenimports += collect_submodules('ckcc')
       +hiddenimports += collect_submodules('bitbox02')
        hiddenimports += ['PyQt5.QtPrintSupport']  # needed by Revealer
        
        datas = [
       t@@ -81,6 +82,7 @@ datas += collect_data_files('safetlib')
        datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
        datas += collect_data_files('ckcc')
       +datas += collect_data_files('bitbox02')
        datas += collect_data_files('jsonrpcserver')
        datas += collect_data_files('jsonrpcclient')
        
   DIR diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
       t@@ -13,4 +13,5 @@ safet>=0.1.5
        keepkey>=6.3.1
        btchip-python>=0.1.26
        ckcc-protocol>=0.7.7
       +bitbox02>=2.0.2
        hidapi
   DIR diff --git a/contrib/udev/53-hid-bitbox02.rules b/contrib/udev/53-hid-bitbox02.rules
       t@@ -0,0 +1 @@
       +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403"
   DIR diff --git a/contrib/udev/54-hid-bitbox02.rules b/contrib/udev/54-hid-bitbox02.rules
       t@@ -0,0 +1 @@
       +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n"
   DIR diff --git a/contrib/udev/README.md b/contrib/udev/README.md
       t@@ -6,7 +6,8 @@ These are necessary for the devices to be usable on Linux environments.
        
         - `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules
         - `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules
       - - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux
       + - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh
       + - `53-hid-bitbox02.rules`, `54-hid-bitbox02.rules` (BitBox02): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh
         - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules
         - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules
         - `51-safe-t.rules` (Archos): https://github.com/archos-safe-t/safe-t-common/blob/master/udev/51-safe-t.rules
   DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
       t@@ -25,7 +25,8 @@
        
        import hashlib
        from typing import List, Tuple, TYPE_CHECKING, Optional, Union
       -from enum import IntEnum
       +import enum
       +from enum import IntEnum, Enum
        
        from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict
        from . import version
       t@@ -432,6 +433,40 @@ def address_to_script(addr: str, *, net=None) -> str:
                raise BitcoinException(f'unknown address type: {addrtype}')
            return script
        
       +
       +class OnchainOutputType(Enum):
       +    """Opaque types of scriptPubKeys.
       +    In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc.
       +    """
       +    P2PKH = enum.auto()
       +    P2SH = enum.auto()
       +    WITVER0_P2WPKH = enum.auto()
       +    WITVER0_P2WSH = enum.auto()
       +
       +
       +def address_to_hash(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes]:
       +    """Return (type, pubkey hash / witness program) for an address."""
       +    if net is None: net = constants.net
       +    if not is_address(addr, net=net):
       +        raise BitcoinException(f"invalid bitcoin address: {addr}")
       +    witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
       +    if witprog is not None:
       +        if witver != 0:
       +            raise BitcoinException(f"not implemented handling for witver={witver}")
       +        if len(witprog) == 20:
       +            return OnchainOutputType.WITVER0_P2WPKH, bytes(witprog)
       +        elif len(witprog) == 32:
       +            return OnchainOutputType.WITVER0_P2WSH, bytes(witprog)
       +        else:
       +            raise BitcoinException(f"unexpected length for segwit witver=0 witprog: len={len(witprog)}")
       +    addrtype, hash_160_ = b58_address_to_hash160(addr)
       +    if addrtype == net.ADDRTYPE_P2PKH:
       +        return OnchainOutputType.P2PKH, hash_160_
       +    elif addrtype == net.ADDRTYPE_P2SH:
       +        return OnchainOutputType.P2SH, hash_160_
       +    raise BitcoinException(f"unknown address type: {addrtype}")
       +
       +
        def address_to_scripthash(addr: str) -> str:
            script = address_to_script(addr)
            return script_to_scripthash(script)
   DIR diff --git a/electrum/gui/icons/bitbox02.png b/electrum/gui/icons/bitbox02.png
       Binary files differ.
   DIR diff --git a/electrum/gui/icons/bitbox02_unpaired.png b/electrum/gui/icons/bitbox02_unpaired.png
       Binary files differ.
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -2273,7 +2273,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    def show_mpk(index):
                        mpk_text.setText(mpk_list[index])
                        mpk_text.repaint()  # macOS hack for #4777
       -                
       +
       +            # declare this value such that the hooks can later figure out what to do
       +            labels_clayout = None
                    # only show the combobox in case multiple accounts are available
                    if len(mpk_list) > 1:
                        # only show the combobox if multiple master keys are defined
       t@@ -2288,6 +2290,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                        on_click = lambda clayout: show_mpk(clayout.selected_index())
                        labels_clayout = ChoicesLayout(_("Master Public Keys"), labels, on_click)
                        vbox.addLayout(labels_clayout.layout())
       +                labels_clayout.selected_index()
                    else:
                        vbox.addWidget(QLabel(_("Master Public Key")))
        
       t@@ -2295,7 +2298,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    vbox.addWidget(mpk_text)
        
                vbox.addStretch(1)
       -        btns = run_hook('wallet_info_buttons', self, dialog) or Buttons(CloseButton(dialog))
       +        btn_export_info = run_hook('wallet_info_buttons', self, dialog)
       +        btn_show_xpub = run_hook('show_xpub_button', self, dialog, labels_clayout)
       +        btn_close = CloseButton(dialog)
       +        btns = Buttons(btn_export_info, btn_show_xpub, btn_close)
                vbox.addLayout(btns)
                dialog.setLayout(vbox)
                dialog.exec_()
   DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
       t@@ -161,6 +161,8 @@ class Buttons(QHBoxLayout):
                QHBoxLayout.__init__(self)
                self.addStretch(1)
                for b in buttons:
       +            if b is None:
       +                continue
                    self.addWidget(b)
        
        class CloseButton(QPushButton):
   DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
       t@@ -360,9 +360,8 @@ class DeviceMgr(ThreadJob):
                # A list of clients.  The key is the client, the value is
                # a (path, id_) pair. Needs self.lock.
                self.clients = {}  # type: Dict[HardwareClientBase, Tuple[Union[str, bytes], str]]
       -        # What we recognise.  Each entry is a (vendor_id, product_id)
       -        # pair.
       -        self.recognised_hardware = set()
       +        # What we recognise.  (vendor_id, product_id) -> Plugin
       +        self._recognised_hardware = {}  # type: Dict[Tuple[int, int], HW_PluginBase]
                # Custom enumerate functions for devices we don't know about.
                self._enumerate_func = set()  # Needs self.lock.
                # locks: if you need to take multiple ones, acquire them in the order they are defined here!
       t@@ -390,9 +389,9 @@ class DeviceMgr(ThreadJob):
                for client in clients:
                    client.timeout(cutoff)
        
       -    def register_devices(self, device_pairs):
       +    def register_devices(self, device_pairs, *, plugin: 'HW_PluginBase'):
                for pair in device_pairs:
       -            self.recognised_hardware.add(pair)
       +            self._recognised_hardware[pair] = plugin
        
            def register_enumerate_func(self, func):
                with self.lock:
       t@@ -642,20 +641,10 @@ class DeviceMgr(ThreadJob):
                devices = []
                for d in hid_list:
                    product_key = (d['vendor_id'], d['product_id'])
       -            if product_key in self.recognised_hardware:
       -                # Older versions of hid don't provide interface_number
       -                interface_number = d.get('interface_number', -1)
       -                usage_page = d['usage_page']
       -                id_ = d['serial_number']
       -                if len(id_) == 0:
       -                    id_ = str(d['path'])
       -                id_ += str(interface_number) + str(usage_page)
       -                devices.append(Device(path=d['path'],
       -                                      interface_number=interface_number,
       -                                      id_=id_,
       -                                      product_key=product_key,
       -                                      usage_page=usage_page,
       -                                      transport_ui_string='hid'))
       +            if product_key in self._recognised_hardware:
       +                plugin = self._recognised_hardware[product_key]
       +                device = plugin.create_device_from_hid_enumeration(d, product_key=product_key)
       +                devices.append(device)
                return devices
        
            @with_scan_lock
   DIR diff --git a/electrum/plugins/bitbox02/__init__.py b/electrum/plugins/bitbox02/__init__.py
       t@@ -0,0 +1,14 @@
       +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"]
   DIR diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py
       t@@ -0,0 +1,617 @@
       +#
       +# BitBox02 Electrum plugin code.
       +#
       +
       +import hid
       +from typing import TYPE_CHECKING, Dict, Tuple, Optional, List, Any, Callable
       +
       +from electrum import bip32, constants
       +from electrum.i18n import _
       +from electrum.keystore import Hardware_KeyStore
       +from electrum.transaction import PartialTransaction
       +from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wallet
       +from electrum.util import bh2u, UserFacingException
       +from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard
       +from electrum.logging import get_logger
       +from electrum.plugin import Device, DeviceInfo
       +from electrum.simple_config import SimpleConfig
       +from electrum.json_db import StoredDict
       +from electrum.storage import get_derivation_used_for_hw_device_encryption
       +from electrum.bitcoin import OnchainOutputType
       +
       +import electrum.bitcoin as bitcoin
       +import electrum.ecc as ecc
       +
       +from ..hw_wallet import HW_PluginBase, HardwareClientBase
       +
       +
       +try:
       +    from bitbox02 import bitbox02
       +    from bitbox02 import util
       +    from bitbox02.communication import (
       +        devices,
       +        HARDENED,
       +        u2fhid,
       +        bitbox_api_protocol,
       +    )
       +    requirements_ok = True
       +except ImportError:
       +    requirements_ok = False
       +
       +
       +_logger = get_logger(__name__)
       +
       +
       +class BitBox02Client(HardwareClientBase):
       +    # handler is a BitBox02_Handler, importing it would lead to a circular dependency
       +    def __init__(self, handler: Any, device: Device, config: SimpleConfig):
       +        self.bitbox02_device = None
       +        self.handler = handler
       +        self.device_descriptor = device
       +        self.config = config
       +        self.bitbox_hid_info = None
       +        if self.config.get("bitbox02") is None:
       +            bitbox02_config: dict = {
       +                "remote_static_noise_keys": [],
       +                "noise_privkey": None,
       +            }
       +            self.config.set_key("bitbox02", bitbox02_config)
       +
       +        bitboxes = devices.get_any_bitbox02s()
       +        for bitbox in bitboxes:
       +            if (
       +                bitbox["path"] == self.device_descriptor.path
       +                and bitbox["interface_number"]
       +                == self.device_descriptor.interface_number
       +            ):
       +                self.bitbox_hid_info = bitbox
       +        if self.bitbox_hid_info is None:
       +            raise Exception("No BitBox02 detected")
       +
       +    def is_initialized(self) -> bool:
       +        return True
       +
       +    def close(self):
       +        try:
       +            self.bitbox02_device.close()
       +        except:
       +            pass
       +
       +    def has_usable_connection_with_device(self) -> bool:
       +        if self.bitbox_hid_info is None:
       +            return False
       +        return True
       +
       +    def pairing_dialog(self, wizard: bool = True):
       +        def pairing_step(code: str, device_response: Callable[[], bool]) -> bool:
       +            msg = "Please compare and confirm the pairing code on your BitBox02:\n" + code
       +            self.handler.show_message(msg)
       +            try:
       +                res = device_response()
       +            except:
       +                # Close the hid device on exception
       +                hid_device.close()
       +                raise
       +            finally:
       +                self.handler.finished()
       +            return res
       +
       +        def exists_remote_static_pubkey(pubkey: bytes) -> bool:
       +            bitbox02_config = self.config.get("bitbox02")
       +            noise_keys = bitbox02_config.get("remote_static_noise_keys")
       +            if noise_keys is not None:
       +                if pubkey.hex() in [noise_key for noise_key in noise_keys]:
       +                    return True
       +            return False
       +
       +        def set_remote_static_pubkey(pubkey: bytes) -> None:
       +            if not exists_remote_static_pubkey(pubkey):
       +                bitbox02_config = self.config.get("bitbox02")
       +                if bitbox02_config.get("remote_static_noise_keys") is not None:
       +                    bitbox02_config["remote_static_noise_keys"].append(pubkey.hex())
       +                else:
       +                    bitbox02_config["remote_static_noise_keys"] = [pubkey.hex()]
       +                self.config.set_key("bitbox02", bitbox02_config)
       +
       +        def get_noise_privkey() -> Optional[bytes]:
       +            bitbox02_config = self.config.get("bitbox02")
       +            privkey = bitbox02_config.get("noise_privkey")
       +            if privkey is not None:
       +                return bytes.fromhex(privkey)
       +            return None
       +
       +        def set_noise_privkey(privkey: bytes) -> None:
       +            bitbox02_config = self.config.get("bitbox02")
       +            bitbox02_config["noise_privkey"] = privkey.hex()
       +            self.config.set_key("bitbox02", bitbox02_config)
       +
       +        def attestation_warning() -> None:
       +            self.handler.show_error(
       +                "The BitBox02 attestation failed.\nTry reconnecting the BitBox02.\nWarning: The device might not be genuine, if the\n problem persists please contact Shift support.",
       +                blocking=True
       +            )
       +
       +        class NoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig):
       +            """NoiseConfig extends BitBoxNoiseConfig"""
       +
       +            def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
       +                return pairing_step(code, device_response)
       +
       +            def attestation_check(self, result: bool) -> None:
       +                if not result:
       +                    attestation_warning()
       +
       +            def contains_device_static_pubkey(self, pubkey: bytes) -> bool:
       +                return exists_remote_static_pubkey(pubkey)
       +
       +            def add_device_static_pubkey(self, pubkey: bytes) -> None:
       +                return set_remote_static_pubkey(pubkey)
       +
       +            def get_app_static_privkey(self) -> Optional[bytes]:
       +                return get_noise_privkey()
       +
       +            def set_app_static_privkey(self, privkey: bytes) -> None:
       +                return set_noise_privkey(privkey)
       +
       +        if self.bitbox02_device is None:
       +            hid_device = hid.device()
       +            hid_device.open_path(self.bitbox_hid_info["path"])
       +
       +            self.bitbox02_device = bitbox02.BitBox02(
       +                transport=u2fhid.U2FHid(hid_device),
       +                device_info=self.bitbox_hid_info,
       +                noise_config=NoiseConfig(),
       +            )
       +
       +        self.fail_if_not_initialized()
       +
       +    def fail_if_not_initialized(self) -> None:
       +        assert self.bitbox02_device
       +        if not self.bitbox02_device.device_info()["initialized"]:
       +            raise Exception(
       +                "Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum"
       +            )
       +
       +    def check_device_firmware_version(self) -> bool:
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +        return self.bitbox02_device.check_firmware_version()
       +
       +    def coin_network_from_electrum_network(self) -> int:
       +        if constants.net.TESTNET:
       +            return bitbox02.btc.TBTC
       +        return bitbox02.btc.BTC
       +
       +    def get_password_for_storage_encryption(self) -> str:
       +        derivation = get_derivation_used_for_hw_device_encryption()
       +        derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation)
       +        xpub = self.bitbox02_device.electrum_encryption_key(derivation_list)
       +        node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(())
       +        return node.eckey.get_public_key_bytes(compressed=True).hex()
       +
       +    def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str:
       +        if self.bitbox02_device is None:
       +            self.pairing_dialog(wizard=False)
       +
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +
       +        self.fail_if_not_initialized()
       +
       +        xpub_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path)
       +        coin_network = self.coin_network_from_electrum_network()
       +
       +        if xtype == "p2wpkh":
       +            if coin_network == bitbox02.btc.BTC:
       +                out_type = bitbox02.btc.BTCPubRequest.ZPUB
       +            else:
       +                out_type = bitbox02.btc.BTCPubRequest.VPUB
       +        elif xtype == "p2wpkh-p2sh":
       +            if coin_network == bitbox02.btc.BTC:
       +                out_type = bitbox02.btc.BTCPubRequest.YPUB
       +            else:
       +                out_type = bitbox02.btc.BTCPubRequest.UPUB
       +        elif xtype == "p2wsh":
       +            if coin_network == bitbox02.btc.BTC:
       +                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_ZPUB
       +            else:
       +                out_type = bitbox02.btc.BTCPubRequest.CAPITAL_VPUB
       +        # The other legacy types are not supported
       +        else:
       +            raise Exception("invalid xtype:{}".format(xtype))
       +
       +        return self.bitbox02_device.btc_xpub(
       +            keypath=xpub_keypath,
       +            xpub_type=out_type,
       +            coin=coin_network,
       +            display=display,
       +        )
       +
       +    def request_root_fingerprint_from_device(self) -> str:
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +
       +        return self.bitbox02_device.root_fingerprint().hex()
       +
       +    def is_pairable(self) -> bool:
       +        if self.bitbox_hid_info is None:
       +            return False
       +        return True
       +
       +    def btc_multisig_config(
       +        self, coin, bip32_path: List[int], wallet: Multisig_Wallet
       +    ):
       +        """
       +        Set and get a multisig config with the current device and some other arbitrary xpubs.
       +        Registers it on the device if not already registered.
       +        """
       +
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +
       +        account_keypath = bip32_path[:4]
       +        xpubs = wallet.get_master_public_keys()
       +        our_xpub = self.get_xpub(
       +            bip32.convert_bip32_intpath_to_strpath(account_keypath), "p2wsh"
       +        )
       +
       +        multisig_config = bitbox02.btc.BTCScriptConfig(
       +            multisig=bitbox02.btc.BTCScriptConfig.Multisig(
       +                threshold=wallet.m,
       +                xpubs=[util.parse_xpub(xpub) for xpub in xpubs],
       +                our_xpub_index=xpubs.index(our_xpub),
       +            )
       +        )
       +
       +        is_registered = self.bitbox02_device.btc_is_script_config_registered(
       +            coin, multisig_config, account_keypath
       +        )
       +        if not is_registered:
       +            name = self.handler.name_multisig_account()
       +            try:
       +                self.bitbox02_device.btc_register_script_config(
       +                    coin=coin,
       +                    script_config=multisig_config,
       +                    keypath=account_keypath,
       +                    name=name,
       +                )
       +            except bitbox02.DuplicateEntryException:
       +                raise
       +            except:
       +                raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02")
       +        return multisig_config
       +
       +    def show_address(
       +        self, bip32_path: str, address_type: str, wallet: Deterministic_Wallet
       +    ) -> str:
       +
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +
       +        address_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path)
       +        coin_network = self.coin_network_from_electrum_network()
       +
       +        if address_type == "p2wpkh":
       +            script_config = bitbox02.btc.BTCScriptConfig(
       +                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
       +            )
       +        elif address_type == "p2wpkh-p2sh":
       +            script_config = bitbox02.btc.BTCScriptConfig(
       +                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
       +            )
       +        elif address_type == "p2wsh":
       +            if type(wallet) is Multisig_Wallet:
       +                script_config = self.btc_multisig_config(
       +                    coin_network, address_keypath, wallet
       +                )
       +            else:
       +                raise Exception("Can only use p2wsh with multisig wallets")
       +        else:
       +            raise Exception(
       +                "invalid address xtype: {} is not supported by the BitBox02".format(
       +                    address_type
       +                )
       +            )
       +
       +        return self.bitbox02_device.btc_address(
       +            keypath=address_keypath,
       +            coin=coin_network,
       +            script_config=script_config,
       +            display=True,
       +        )
       +
       +    def sign_transaction(
       +        self,
       +        keystore: Hardware_KeyStore,
       +        tx: PartialTransaction,
       +        wallet: Deterministic_Wallet,
       +    ):
       +        if tx.is_complete():
       +            return
       +
       +        if self.bitbox02_device is None:
       +            raise Exception(
       +                "Need to setup communication first before attempting any BitBox02 calls"
       +            )
       +
       +        coin = bitbox02.btc.BTC
       +        if constants.net.TESTNET:
       +            coin = bitbox02.btc.TBTC
       +
       +        tx_script_type = None
       +
       +        # Build BTCInputType list
       +        inputs = []
       +        for txin in tx.inputs():
       +            _, full_path = keystore.find_my_pubkey_in_txinout(txin)
       +
       +            if full_path is None:
       +                raise Exception(
       +                    "A wallet owned pubkey was not found in the transaction input to be signed"
       +                )
       +
       +            inputs.append(
       +                {
       +                    "prev_out_hash": txin.prevout.txid[::-1],
       +                    "prev_out_index": txin.prevout.out_idx,
       +                    "prev_out_value": txin.value_sats(),
       +                    "sequence": txin.nsequence,
       +                    "keypath": full_path,
       +                }
       +            )
       +
       +            if tx_script_type == None:
       +                tx_script_type = txin.script_type
       +            elif tx_script_type != txin.script_type:
       +                raise Exception("Cannot mix different input script types")
       +
       +        if tx_script_type == "p2wpkh":
       +            tx_script_type = bitbox02.btc.BTCScriptConfig(
       +                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
       +            )
       +        elif tx_script_type == "p2wpkh-p2sh":
       +            tx_script_type = bitbox02.btc.BTCScriptConfig(
       +                simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
       +            )
       +        elif tx_script_type == "p2wsh":
       +            if type(wallet) is Multisig_Wallet:
       +                tx_script_type = self.btc_multisig_config(coin, full_path, wallet)
       +            else:
       +                raise Exception("Can only use p2wsh with multisig wallets")
       +        else:
       +            raise UserFacingException(
       +                "invalid input script type: {} is not supported by the BitBox02".format(
       +                    tx_script_type
       +                )
       +            )
       +
       +        # Build BTCOutputType list
       +        outputs = []
       +        for txout in tx.outputs():
       +            assert txout.address
       +            # check for change
       +            if txout.is_change:
       +                _, change_pubkey_path = keystore.find_my_pubkey_in_txinout(txout)
       +                outputs.append(
       +                    bitbox02.BTCOutputInternal(
       +                        keypath=change_pubkey_path, value=txout.value,
       +                    )
       +                )
       +            else:
       +                addrtype, pubkey_hash = bitcoin.address_to_hash(txout.address)
       +                if addrtype == OnchainOutputType.P2PKH:
       +                    output_type = bitbox02.btc.P2PKH
       +                elif addrtype == OnchainOutputType.P2SH:
       +                    output_type = bitbox02.btc.P2SH
       +                elif addrtype == OnchainOutputType.WITVER0_P2WPKH:
       +                    output_type = bitbox02.btc.P2WPKH
       +                elif addrtype == OnchainOutputType.WITVER0_P2WSH:
       +                    output_type = bitbox02.btc.P2WSH
       +                else:
       +                    raise UserFacingException(
       +                        "Received unsupported output type during transaction signing: {} is not supported by the BitBox02".format(
       +                            addrtype
       +                        )
       +                    )
       +                outputs.append(
       +                    bitbox02.BTCOutputExternal(
       +                        output_type=output_type,
       +                        output_hash=pubkey_hash,
       +                        value=txout.value,
       +                    )
       +                )
       +
       +        if type(wallet) is Standard_Wallet:
       +            keypath_account = full_path[:3]
       +        elif type(wallet) is Multisig_Wallet:
       +            keypath_account = full_path[:4]
       +        else:
       +            raise Exception(
       +                "BitBox02 does not support this wallet type: {}".format(type(wallet))
       +            )
       +
       +        sigs = self.bitbox02_device.btc_sign(
       +            coin,
       +            tx_script_type,
       +            keypath_account=keypath_account,
       +            inputs=inputs,
       +            outputs=outputs,
       +            locktime=tx.locktime,
       +            version=tx.version,
       +        )
       +
       +        # Fill signatures
       +        if len(sigs) != len(tx.inputs()):
       +            raise Exception("Incorrect number of inputs signed.")  # Should never occur
       +        signatures = [bh2u(ecc.der_sig_from_sig_string(x[1])) + "01" for x in sigs]
       +        tx.update_signatures(signatures)
       +
       +
       +class BitBox02_KeyStore(Hardware_KeyStore):
       +    hw_type = "bitbox02"
       +    device = "BitBox02"
       +    plugin: "BitBox02Plugin"
       +
       +    def __init__(self, d: StoredDict):
       +        super().__init__(d)
       +        self.force_watching_only = False
       +        self.ux_busy = False
       +
       +    def get_client(self):
       +        return self.plugin.get_client(self)
       +
       +    def give_error(self, message: Exception, clear_client: bool = False):
       +        self.logger.info(message)
       +        if not self.ux_busy:
       +            self.handler.show_error(message)
       +        else:
       +            self.ux_busy = False
       +        if clear_client:
       +            self.client = None
       +        raise UserFacingException(message)
       +
       +    def decrypt_message(self, pubkey, message, password):
       +        raise UserFacingException(
       +            _(
       +                "Message encryption, decryption and signing are currently not supported for {}"
       +            ).format(self.device)
       +        )
       +
       +    def sign_message(self, sequence, message, password):
       +        raise UserFacingException(
       +            _(
       +                "Message encryption, decryption and signing are currently not supported for {}"
       +            ).format(self.device)
       +        )
       +
       +    def sign_transaction(self, tx: PartialTransaction, password: str):
       +        if tx.is_complete():
       +            return
       +        client = self.get_client()
       +        assert isinstance(client, BitBox02Client)
       +
       +        try:
       +            try:
       +                self.handler.show_message("Authorize Transaction...")
       +                client.sign_transaction(self, tx, self.handler.get_wallet())
       +
       +            finally:
       +                self.handler.finished()
       +
       +        except Exception as e:
       +            self.logger.exception("")
       +            self.give_error(e, True)
       +            return
       +
       +    def show_address(
       +        self, sequence: Tuple[int, int], txin_type: str, wallet: Deterministic_Wallet
       +    ):
       +        client = self.get_client()
       +        address_path = "{}/{}/{}".format(
       +            self.get_derivation_prefix(), sequence[0], sequence[1]
       +        )
       +        try:
       +            try:
       +                self.handler.show_message(_("Showing address ..."))
       +                dev_addr = client.show_address(address_path, txin_type, wallet)
       +            finally:
       +                self.handler.finished()
       +        except Exception as e:
       +            self.logger.exception("")
       +            self.handler.show_error(e)
       +
       +class BitBox02Plugin(HW_PluginBase):
       +    keystore_class = BitBox02_KeyStore
       +    minimum_library = (2, 0, 2)
       +    DEVICE_IDS = [(0x03EB, 0x2403)]
       +
       +    SUPPORTED_XTYPES = ("p2wpkh-p2sh", "p2wpkh", "p2wsh")
       +
       +    def __init__(self, parent: HW_PluginBase, config: SimpleConfig, name: str):
       +        super().__init__(parent, config, name)
       +
       +        self.libraries_available = self.check_libraries_available()
       +        if not self.libraries_available:
       +            return
       +        self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)
       +
       +    def get_library_version(self):
       +        try:
       +            from bitbox02 import bitbox02
       +            version = bitbox02.__version__
       +        except:
       +            version = "unknown"
       +        if requirements_ok:
       +            return version
       +        else:
       +            raise ImportError()
       +
       +
       +    # handler is a BitBox02_Handler
       +    def create_client(self, device: Device, handler: Any) -> BitBox02Client:
       +        if not handler:
       +            self.handler = handler
       +        return BitBox02Client(handler, device, self.config)
       +
       +    def setup_device(
       +        self, device_info: DeviceInfo, wizard: BaseWizard, purpose: int
       +    ):
       +        device_id = device_info.device.id_
       +        client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard)
       +        assert isinstance(client, BitBox02Client)
       +        if client.bitbox02_device is None:
       +            wizard.run_task_without_blocking_gui(
       +                task=lambda client=client: client.pairing_dialog())
       +        client.fail_if_not_initialized()
       +        return client
       +
       +    def get_xpub(
       +        self, device_id: str, derivation: str, xtype: str, wizard: BaseWizard
       +    ):
       +        if xtype not in self.SUPPORTED_XTYPES:
       +            raise ScriptTypeNotSupported(
       +                _("This type of script is not supported with {}.").format(self.device)
       +            )
       +        client = self.scan_and_create_client_for_device(device_id=device_id, wizard=wizard)
       +        assert isinstance(client, BitBox02Client)
       +        assert client.bitbox02_device is not None
       +        return client.get_xpub(derivation, xtype)
       +
       +    def show_address(
       +        self,
       +        wallet: Deterministic_Wallet,
       +        address: str,
       +        keystore: BitBox02_KeyStore = None,
       +    ):
       +        if keystore is None:
       +            keystore = wallet.get_keystore()
       +        if not self.show_address_helper(wallet, address, keystore):
       +            return
       +
       +        txin_type = wallet.get_txin_type(address)
       +        sequence = wallet.get_address_index(address)
       +        keystore.show_address(sequence, txin_type, wallet)
       +
       +    def show_xpub(self, keystore: BitBox02_KeyStore):
       +        client = keystore.get_client()
       +        assert isinstance(client, BitBox02Client)
       +        derivation = keystore.get_derivation_prefix()
       +        xtype = keystore.get_bip32_node_for_xpub().xtype
       +        client.get_xpub(derivation, xtype, display=True)
       +
       +    def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device':
       +        device = super().create_device_from_hid_enumeration(d, product_key=product_key)
       +        # The BitBox02's product_id is not unique per device, thus use the path instead to
       +        # distinguish devices.
       +        id_ = str(d['path'])
       +        return device._replace(id_=id_)
   DIR diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py
       t@@ -0,0 +1,127 @@
       +from functools import partial
       +
       +from PyQt5.QtWidgets import (
       +    QPushButton,
       +    QLabel,
       +    QVBoxLayout,
       +    QLineEdit,
       +    QHBoxLayout,
       +)
       +
       +from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot
       +
       +from electrum.gui.qt.util import (
       +    WindowModalDialog,
       +    OkButton,
       +)
       +
       +from electrum.i18n import _
       +from electrum.plugin import hook
       +
       +from .bitbox02 import BitBox02Plugin
       +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
       +from ..hw_wallet.plugin import only_hook_if_libraries_available
       +
       +
       +class Plugin(BitBox02Plugin, QtPluginBase):
       +    icon_unpaired = "bitbox02_unpaired.png"
       +    icon_paired = "bitbox02.png"
       +
       +    def create_handler(self, window):
       +        return BitBox02_Handler(window)
       +
       +    @only_hook_if_libraries_available
       +    @hook
       +    def receive_menu(self, menu, addrs, wallet):
       +        # Context menu on each address in the Addresses Tab, right click...
       +        if len(addrs) != 1:
       +            return
       +        for keystore in wallet.get_keystores():
       +            if type(keystore) == self.keystore_class:
       +
       +                def show_address(keystore=keystore):
       +                    keystore.thread.add(
       +                        partial(self.show_address, wallet, addrs[0], keystore=keystore)
       +                    )
       +
       +                device_name = "{} ({})".format(self.device, keystore.label)
       +                menu.addAction(_("Show on {}").format(device_name), show_address)
       +
       +    @only_hook_if_libraries_available
       +    @hook
       +    def show_xpub_button(self, main_window, dialog, labels_clayout):
       +        # user is about to see the "Wallet Information" dialog
       +        # - add a button to show the xpub on the BitBox02 device
       +        wallet = main_window.wallet
       +        if not any(type(ks) == self.keystore_class for ks in wallet.get_keystores()):
       +            # doesn't involve a BitBox02 wallet, hide feature
       +            return
       +
       +        btn = QPushButton(_("Show on BitBox02"))
       +
       +        def on_button_click():
       +            selected_keystore_index = 0
       +            if labels_clayout is not None:
       +                selected_keystore_index = labels_clayout.selected_index()
       +            keystores = wallet.get_keystores()
       +            selected_keystore = keystores[selected_keystore_index]
       +            if type(selected_keystore) != self.keystore_class:
       +                main_window.show_error("Select a BitBox02 xpub")
       +                return
       +            selected_keystore.thread.add(
       +                partial(self.show_xpub, keystore=selected_keystore)
       +            )
       +
       +        btn.clicked.connect(lambda unused: on_button_click())
       +        return btn
       +
       +
       +class BitBox02_Handler(QtHandlerBase):
       +
       +    def __init__(self, win):
       +        super(BitBox02_Handler, self).__init__(win, "BitBox02")
       +
       +    def message_dialog(self, msg):
       +        self.clear_dialog()
       +        self.dialog = dialog = WindowModalDialog(
       +            self.top_level_window(), _("BitBox02 Status")
       +        )
       +        l = QLabel(msg)
       +        vbox = QVBoxLayout(dialog)
       +        vbox.addWidget(l)
       +        dialog.show()
       +
       +    def name_multisig_account(self):
       +        return QMetaObject.invokeMethod(
       +            self,
       +            "_name_multisig_account",
       +            Qt.BlockingQueuedConnection,
       +            Q_RETURN_ARG(str),
       +        )
       +
       +    @pyqtSlot(result=str)
       +    def _name_multisig_account(self):
       +        dialog = WindowModalDialog(None, "Create Multisig Account")
       +        vbox = QVBoxLayout()
       +        label = QLabel(
       +            _(
       +                "Enter a descriptive name for your multisig account.\nYou should later be able to use the name to uniquely identify this multisig account"
       +            )
       +        )
       +        hl = QHBoxLayout()
       +        hl.addWidget(label)
       +        name = QLineEdit()
       +        name.setMaxLength(30)
       +        name.resize(200, 40)
       +        he = QHBoxLayout()
       +        he.addWidget(name)
       +        okButton = OkButton(dialog)
       +        hlb = QHBoxLayout()
       +        hlb.addWidget(okButton)
       +        hlb.addStretch(2)
       +        vbox.addLayout(hl)
       +        vbox.addLayout(he)
       +        vbox.addLayout(hlb)
       +        dialog.setLayout(vbox)
       +        dialog.exec_()
       +        return name.text().strip()
   DIR diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py
       t@@ -28,12 +28,6 @@ class ColdcardCmdLineHandler(CmdLineHandler):
            def stop(self):
                pass
        
       -    def show_message(self, msg, on_cancel=None):
       -        print_stderr(msg)
       -
       -    def show_error(self, msg, blocking=False):
       -        print_stderr(msg)
       -
            def update_status(self, b):
                _logger.info(f'hw device status {b}')
        
   DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
       t@@ -477,7 +477,7 @@ class ColdcardPlugin(HW_PluginBase):
                if not self.libraries_available:
                    return
        
       -        self.device_manager().register_devices(self.DEVICE_IDS)
       +        self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)
                self.device_manager().register_enumerate_func(self.detect_simulator)
        
            def get_library_version(self):
   DIR diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
       t@@ -57,7 +57,7 @@ class Plugin(ColdcardPlugin, QtPluginBase):
                btn = QPushButton(_("Export for Coldcard"))
                btn.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet))
        
       -        return Buttons(btn, CloseButton(dialog))
       +        return btn
        
            def export_multisig_setup(self, main_window, wallet):
        
       t@@ -77,15 +77,10 @@ class Plugin(ColdcardPlugin, QtPluginBase):
        
        
        class Coldcard_Handler(QtHandlerBase):
       -    setup_signal = pyqtSignal()
       -    #auth_signal = pyqtSignal(object)
        
            def __init__(self, win):
                super(Coldcard_Handler, self).__init__(win, 'Coldcard')
       -        self.setup_signal.connect(self.setup_dialog)
       -        #self.auth_signal.connect(self.auth_dialog)
        
       -    
            def message_dialog(self, msg):
                self.clear_dialog()
                self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status"))
       t@@ -93,16 +88,7 @@ class Coldcard_Handler(QtHandlerBase):
                vbox = QVBoxLayout(dialog)
                vbox.addWidget(l)
                dialog.show()
       -        
       -    def get_setup(self):
       -        self.done.clear()
       -        self.setup_signal.emit()
       -        self.done.wait()
       -        return 
       -        
       -    def setup_dialog(self):
       -        self.show_error(_('Please initialize your Coldcard while disconnected.'))
       -        return
       +
        
        class CKCCSettingsDialog(WindowModalDialog):
        
   DIR diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
       t@@ -675,7 +675,7 @@ class DigitalBitboxPlugin(HW_PluginBase):
            def __init__(self, parent, config, name):
                HW_PluginBase.__init__(self, parent, config, name)
                if self.libraries_available:
       -            self.device_manager().register_devices(self.DEVICE_IDS)
       +            self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)
        
                self.digitalbitbox_config = self.config.get('digitalbitbox', {})
        
   DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
       t@@ -60,6 +60,22 @@ class HW_PluginBase(BasePlugin):
            def device_manager(self) -> 'DeviceMgr':
                return self.parent.device_manager
        
       +    def create_device_from_hid_enumeration(self, d: dict, *, product_key) -> 'Device':
       +        # Older versions of hid don't provide interface_number
       +        interface_number = d.get('interface_number', -1)
       +        usage_page = d['usage_page']
       +        id_ = d['serial_number']
       +        if len(id_) == 0:
       +            id_ = str(d['path'])
       +        id_ += str(interface_number) + str(usage_page)
       +        device = Device(path=d['path'],
       +                        interface_number=interface_number,
       +                        id_=id_,
       +                        product_key=product_key,
       +                        usage_page=usage_page,
       +                        transport_ui_string='hid')
       +        return device
       +
            @hook
            def close_wallet(self, wallet: 'Abstract_Wallet'):
                for keystore in wallet.get_keystores():
       t@@ -165,7 +181,7 @@ class HW_PluginBase(BasePlugin):
                              handler: Optional['HardwareHandlerBase']) -> Optional['HardwareClientBase']:
                raise NotImplementedError()
        
       -    def get_xpub(self, device_id, derivation: str, xtype, wizard: 'BaseWizard') -> str:
       +    def get_xpub(self, device_id: str, derivation: str, xtype, wizard: 'BaseWizard') -> str:
                raise NotImplementedError()
        
            def create_handler(self, window) -> 'HardwareHandlerBase':
   DIR diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
       t@@ -88,7 +88,7 @@ class KeepKeyPlugin(HW_PluginBase):
                    self.DEVICE_IDS = (keepkeylib.transport_hid.DEVICE_IDS +
                                       keepkeylib.transport_webusb.DEVICE_IDS)
                    # only "register" hid device id:
       -            self.device_manager().register_devices(keepkeylib.transport_hid.DEVICE_IDS)
       +            self.device_manager().register_devices(keepkeylib.transport_hid.DEVICE_IDS, plugin=self)
                    # for webusb transport, use custom enumerate function:
                    self.device_manager().register_enumerate_func(self.enumerate)
                    self.libraries_available = True
   DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
       t@@ -578,7 +578,7 @@ class LedgerPlugin(HW_PluginBase):
                self.segwit = config.get("segwit")
                HW_PluginBase.__init__(self, parent, config, name)
                if self.libraries_available:
       -            self.device_manager().register_devices(self.DEVICE_IDS)
       +            self.device_manager().register_devices(self.DEVICE_IDS, plugin=self)
        
            def get_btchip_device(self, device):
                ledger = False