electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 3745f35f69ff9d2add6b94ce27cfca12baa65e47
parent bfffc7cb1ec3372d771371b5731226d1a5c8bafa
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:
Mcontrib/build-wine/deterministic.spec | 2++
Mcontrib/deterministic-build/requirements-hw.txt | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontrib/osx/osx.spec | 2++
Mcontrib/requirements/requirements-hw.txt | 1+
Acontrib/udev/53-hid-bitbox02.rules | 1+
Acontrib/udev/54-hid-bitbox02.rules | 1+
Mcontrib/udev/README.md | 3++-
Melectrum/bitcoin.py | 37++++++++++++++++++++++++++++++++++++-
Aelectrum/gui/icons/bitbox02.png | 0
Aelectrum/gui/icons/bitbox02_unpaired.png | 0
Melectrum/gui/qt/main_window.py | 10++++++++--
Melectrum/gui/qt/util.py | 2++
Melectrum/plugin.py | 27++++++++-------------------
Aelectrum/plugins/bitbox02/__init__.py | 14++++++++++++++
Aelectrum/plugins/bitbox02/bitbox02.py | 617+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/plugins/bitbox02/qt.py | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/plugins/coldcard/cmdline.py | 6------
Melectrum/plugins/coldcard/coldcard.py | 2+-
Melectrum/plugins/coldcard/qt.py | 18++----------------
Melectrum/plugins/digitalbitbox/digitalbitbox.py | 2+-
Melectrum/plugins/hw_wallet/plugin.py | 18+++++++++++++++++-
Melectrum/plugins/keepkey/keepkey.py | 2+-
Melectrum/plugins/ledger/ledger.py | 2+-
23 files changed, 907 insertions(+), 50 deletions(-)

diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec @@ -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 @@ -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') diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt @@ -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 @@ -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 \ @@ -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 @@ -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 diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec @@ -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 = [ @@ -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') diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt @@ -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 diff --git a/contrib/udev/53-hid-bitbox02.rules b/contrib/udev/53-hid-bitbox02.rules @@ -0,0 +1 @@ +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403" diff --git a/contrib/udev/54-hid-bitbox02.rules b/contrib/udev/54-hid-bitbox02.rules @@ -0,0 +1 @@ +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n" diff --git a/contrib/udev/README.md b/contrib/udev/README.md @@ -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 diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py @@ -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 @@ -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) diff --git a/electrum/gui/icons/bitbox02.png b/electrum/gui/icons/bitbox02.png Binary files differ. diff --git a/electrum/gui/icons/bitbox02_unpaired.png b/electrum/gui/icons/bitbox02_unpaired.png Binary files differ. diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -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 @@ -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"))) @@ -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_() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py @@ -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): diff --git a/electrum/plugin.py b/electrum/plugin.py @@ -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! @@ -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: @@ -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 diff --git a/electrum/plugins/bitbox02/__init__.py b/electrum/plugins/bitbox02/__init__.py @@ -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"] diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py @@ -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_) diff --git a/electrum/plugins/bitbox02/qt.py b/electrum/plugins/bitbox02/qt.py @@ -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() diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py @@ -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}') diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py @@ -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): diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py @@ -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): @@ -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")) @@ -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): diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -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', {}) diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py @@ -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(): @@ -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': diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -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 diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -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