Browse Source

Merge pull request #5993 from TheCharlatan/bitbox02New

BitBox02 Electrum plugin support
hard-fail-on-bad-server-string
ghost43 5 years ago
committed by GitHub
parent
commit
3745f35f69
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      contrib/build-wine/deterministic.spec
  2. 63
      contrib/deterministic-build/requirements-hw.txt
  3. 2
      contrib/osx/osx.spec
  4. 1
      contrib/requirements/requirements-hw.txt
  5. 1
      contrib/udev/53-hid-bitbox02.rules
  6. 1
      contrib/udev/54-hid-bitbox02.rules
  7. 3
      contrib/udev/README.md
  8. 37
      electrum/bitcoin.py
  9. BIN
      electrum/gui/icons/bitbox02.png
  10. BIN
      electrum/gui/icons/bitbox02_unpaired.png
  11. 10
      electrum/gui/qt/main_window.py
  12. 2
      electrum/gui/qt/util.py
  13. 27
      electrum/plugin.py
  14. 14
      electrum/plugins/bitbox02/__init__.py
  15. 617
      electrum/plugins/bitbox02/bitbox02.py
  16. 127
      electrum/plugins/bitbox02/qt.py
  17. 6
      electrum/plugins/coldcard/cmdline.py
  18. 2
      electrum/plugins/coldcard/coldcard.py
  19. 18
      electrum/plugins/coldcard/qt.py
  20. 2
      electrum/plugins/digitalbitbox/digitalbitbox.py
  21. 18
      electrum/plugins/hw_wallet/plugin.py
  22. 2
      electrum/plugins/keepkey/keepkey.py
  23. 2
      electrum/plugins/ledger/ledger.py

2
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')

63
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

2
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')

1
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

1
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"

1
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"

3
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

37
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)

BIN
electrum/gui/icons/bitbox02.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
electrum/gui/icons/bitbox02_unpaired.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

10
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_()

2
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):

27
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

14
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"]

617
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_)

127
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()

6
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}')

2
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):

18
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):

2
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', {})

18
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':

2
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

2
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

Loading…
Cancel
Save