mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 09:21:39 +00:00
Merge pull request #5993 from TheCharlatan/bitbox02New
BitBox02 Electrum plugin support
This commit is contained in:
commit
3745f35f69
23 changed files with 907 additions and 50 deletions
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
Normal file
1
contrib/udev/53-hid-bitbox02.rules
Normal file
|
@ -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
Normal file
1
contrib/udev/54-hid-bitbox02.rules
Normal file
|
@ -0,0 +1 @@
|
|||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n"
|
|
@ -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
|
||||
|
|
|
@ -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
Normal file
BIN
electrum/gui/icons/bitbox02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
electrum/gui/icons/bitbox02_unpaired.png
Normal file
BIN
electrum/gui/icons/bitbox02_unpaired.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -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_()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
Normal file
14
electrum/plugins/bitbox02/__init__.py
Normal file
|
@ -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
Normal file
617
electrum/plugins/bitbox02/bitbox02.py
Normal file
|
@ -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
Normal file
127
electrum/plugins/bitbox02/qt.py
Normal file
|
@ -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()
|
|
@ -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}')
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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', {})
|
||||
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue