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