diff --git a/README.md b/README.md index 334f3cc77..302364298 100755 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ At the moment Specter-Desktop is working with all major hardware wallets includi - Trezor - Ledger - KeepKey +- BitBox02 - ColdCard (optionally airgapped, using an SD card) - Electrum (optionally airgapped, if running Electrum on an airgapped computer/ phone) - Specter DIY (optionally airgapped, using QR codes) diff --git a/requirements.txt b/requirements.txt index 61d6d7aa5..b3dc0b99e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ Flask==1.1.2 Flask-Cors==3.0.8 Flask-Login==0.5.0 hwi==1.1.2 +bitbox02==4.1.0 pyserial==3.4 python-dotenv==0.13.0 requests==2.23.0 diff --git a/src/cryptoadvance/specter/devices/__init__.py b/src/cryptoadvance/specter/devices/__init__.py index bdd753000..0a117b7c6 100644 --- a/src/cryptoadvance/specter/devices/__init__.py +++ b/src/cryptoadvance/specter/devices/__init__.py @@ -1,6 +1,7 @@ from .coldcard import ColdCard from .trezor import Trezor from .ledger import Ledger +from .bitbox02 import BitBox02 from .keepkey import Keepkey from .specter import Specter from .cobo import Cobo @@ -12,6 +13,7 @@ from .bitcoin_core import BitcoinCore __all__ = [ Trezor, Ledger, + BitBox02, Specter, ColdCard, Keepkey, diff --git a/src/cryptoadvance/specter/devices/bitbox02.py b/src/cryptoadvance/specter/devices/bitbox02.py new file mode 100644 index 000000000..ad89ebd64 --- /dev/null +++ b/src/cryptoadvance/specter/devices/bitbox02.py @@ -0,0 +1,18 @@ +from .hwi_device import HWIDevice +from .hwi.bitbox02 import enumerate as bitbox02_enumerate, Bitbox02Client + + +class BitBox02(HWIDevice): + device_type = "bitbox02" + name = "BitBox02" + + def __init__(self, name, alias, keys, fullpath, manager): + HWIDevice.__init__(self, name, alias, keys, fullpath, manager) + + @classmethod + def enumerate(cls, *args, **kwargs): + return bitbox02_enumerate(*args, **kwargs) + + @classmethod + def get_client(cls, *args, **kwargs): + return Bitbox02Client(*args, **kwargs) diff --git a/src/cryptoadvance/specter/devices/hwi/bitbox02.py b/src/cryptoadvance/specter/devices/hwi/bitbox02.py new file mode 100644 index 000000000..f127de17b --- /dev/null +++ b/src/cryptoadvance/specter/devices/hwi/bitbox02.py @@ -0,0 +1,646 @@ +from typing import ( + cast, + Any, + Callable, + Dict, + Optional, + Union, + Tuple, + List, + Sequence, + TypeVar, +) +from binascii import unhexlify +import struct +import builtins +import sys +from functools import wraps + +from hwilib.hwwclient import HardwareWalletClient +from hwilib.descriptor import Descriptor +from hwilib.serializations import ( + PSBT, + CTxOut, + is_p2pkh, + is_p2wpkh, + is_p2wsh, + ser_uint256, + ser_sig_der, +) +from hwilib.errors import ( + HWWError, + ActionCanceledError, + BadArgumentError, + DeviceNotReadyError, + UnavailableActionError, + DEVICE_NOT_INITIALIZED, + handle_errors, + common_err_msgs, +) + +import hid # type: ignore + +from bitbox02 import util +from bitbox02 import bitbox02 +from bitbox02.communication import ( + devices, + u2fhid, + FirmwareVersionOutdatedException, + Bitbox02Exception, + UserAbortException, + HARDENED, + ERR_GENERIC, +) + +from bitbox02.communication.bitbox_api_protocol import ( + Platform, + BitBox02Edition, + BitBoxNoiseConfig, +) + + +class BitBox02Error(UnavailableActionError): + def __init__(self, msg: str): + """ + BitBox02 unexpected error. The BitBox02 does not return give granular error messages, + so we give hints to as what could be wrong. + """ + msg = "Input error: {}. A keypath might be invalid. Supported keypaths are: ".format( + msg + ) + msg += "m/49'/0'/ for p2wpkh-p2sh; " + msg += "m/84'/0'/ for p2wpkh; " + msg += "m/48'/0'//2' for p2wsh multisig; " + msg += "account can be between 0' and 99'; " + msg += "For address keypaths, append /0/
for a receive and /1/ for a change address." + super().__init__(msg) + + +ERR_INVALID_INPUT = 101 + +PURPOSE_P2WPKH_P2SH = 49 + HARDENED +PURPOSE_P2WPKH = 84 + HARDENED +PURPOSE_MULTISIG_P2WSH = 48 + HARDENED + +# External GUI tools using hwi.py as a command line tool to integrate hardware wallets usually do +# not have an actual terminal for IO. +_using_external_gui = not sys.stdout.isatty() +if _using_external_gui: + _unpaired_errmsg = "Device not paired yet. Please pair using the BitBoxApp, then close the BitBoxApp and try again." +else: + _unpaired_errmsg = "Device not paired yet. Please use any subcommand to pair" + + +class SilentNoiseConfig(util.BitBoxAppNoiseConfig): + """ + Used during `enumerate()`. Raises an exception if the device is unpaired. + Attestation check is silent. + + Rationale: enumerate() should not show any dialogs. + """ + + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + raise DeviceNotReadyError(_unpaired_errmsg) + + def attestation_check(self, result: bool) -> None: + pass + + +class CLINoiseConfig(util.BitBoxAppNoiseConfig): + """ Noise pairing and attestation check handling in the terminal (stdin/stdout) """ + + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + if _using_external_gui: + # The user can't see the pairing in the terminal. The + # output format is also not appropriate for parsing by + # external tools doing inter process communication using + # stdin/stdout. For now, we direct the user to pair in the + # BitBoxApp instead. + raise DeviceNotReadyError(_unpaired_errmsg) + + print("Please compare and confirm the pairing code on your BitBox02:") + print(code) + if not device_response(): + return False + return input("Accept pairing? [y]/n: ").strip() != "n" + + def attestation_check(self, result: bool) -> None: + if result: + sys.stderr.write("BitBox02 attestation check PASSED\n") + else: + sys.stderr.write("BitBox02 attestation check FAILED\n") + sys.stderr.write( + "Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.\n" + ) + + +def _parse_path(nstr: str) -> Sequence[int]: + """ + Adapted from trezorlib.tools.parse_path. + Convert BIP32 path string to list of uint32 integers with hardened flags. + Several conventions are supported to set the hardened flag: -1, 1', 1h + + e.g.: "0/1h/1" -> [0, 0x80000001, 1] + + :param nstr: path string + :return: list of integers + """ + if not nstr: + return [] + + n = nstr.split("/") + + # m/a/b/c => a/b/c + if n[0] == "m": + n = n[1:] + + def str_to_harden(x: str) -> int: + if x.startswith("-"): + return abs(int(x)) + HARDENED + elif x.endswith(("h", "'")): + return int(x[:-1]) + HARDENED + else: + return int(x) + + try: + return [str_to_harden(x) for x in n] + except Exception: + raise ValueError("Invalid BIP32 path", nstr) + + +def enumerate(password: str = "") -> List[Dict[str, object]]: + """ + Enumerate all BitBox02 devices. Bootloaders excluded. + """ + result = [] + for device_info in devices.get_any_bitbox02s(): + path = device_info["path"].decode() + client = Bitbox02Client(path) + client.set_noise_config(SilentNoiseConfig()) + version, platform, edition, unlocked = bitbox02.BitBox02.get_info( + client.transport + ) + if platform != Platform.BITBOX02: + client.close() + continue + if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY): + client.close() + continue + + assert isinstance(edition, BitBox02Edition) + + d_data = { + "type": "bitbox02", + "path": path, + "model": { + BitBox02Edition.MULTI: "bitbox02_multi", + BitBox02Edition.BTCONLY: "bitbox02_btconly", + }[edition], + "needs_pin_sent": False, + "needs_passphrase_sent": False, + } + + with handle_errors(common_err_msgs["enumerate"], d_data): + if not unlocked: + raise DeviceNotReadyError( + "Please load wallet to unlock." + if _using_external_gui + else "Please use any subcommand to unlock" + ) + d_data["fingerprint"] = client.get_master_fingerprint_hex() + + result.append(d_data) + + client.close() + return result + + +T = TypeVar("T", bound=Callable[..., Any]) + + +def bitbox02_exception(f: T) -> T: + """ + Maps bitbox02 library exceptions into a HWI exceptions. + """ + + @wraps(f) + def func(*args, **kwargs): # type: ignore + """ Wraps f, mapping exceptions. """ + try: + return f(*args, **kwargs) + except UserAbortException: + raise ActionCanceledError("{} canceled".format(f.__name__)) + except Bitbox02Exception as exc: + if exc.code in (ERR_GENERIC, ERR_INVALID_INPUT): + raise BitBox02Error(str(exc)) + raise exc + except FirmwareVersionOutdatedException as exc: + raise DeviceNotReadyError(str(exc)) + + return cast(T, func) + + +# This class extends the HardwareWalletClient for BitBox02 specific things +class Bitbox02Client(HardwareWalletClient): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: + """ + Initializes a new BitBox02 client instance. + """ + super().__init__(path, password=password, expert=expert) + if password: + raise BadArgumentError( + "The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock." + ) + + hid_device = hid.device() + hid_device.open_path(path.encode()) + self.transport = u2fhid.U2FHid(hid_device) + self.device_path = path + + # use self.init() to access self.bb02. + self.bb02: Optional[bitbox02.BitBox02] = None + + self.noise_config: BitBoxNoiseConfig = CLINoiseConfig() + + def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None: + self.noise_config = noise_config + + def init(self, expect_initialized: bool = True) -> bitbox02.BitBox02: + if self.bb02 is not None: + return self.bb02 + + for device_info in devices.get_any_bitbox02s(): + if device_info["path"].decode() == self.device_path: + bb02 = bitbox02.BitBox02( + transport=self.transport, + device_info=device_info, + noise_config=self.noise_config, + ) + try: + bb02.check_min_version() + except FirmwareVersionOutdatedException as exc: + sys.stderr.write("WARNING: {}\n".format(exc)) + raise + self.bb02 = bb02 + is_initialized = bb02.device_info()["initialized"] + if expect_initialized: + if not is_initialized: + raise HWWError( + "The BitBox02 must be initialized first.", + DEVICE_NOT_INITIALIZED, + ) + elif is_initialized: + raise UnavailableActionError("The BitBox02 must be wiped before setup.") + + return bb02 + raise Exception( + "Could not find the hid device info for path {}".format(self.device_path) + ) + + def close(self) -> None: + self.transport.close() + + def get_master_fingerprint_hex(self) -> str: + """ + HWI by default retrieves the fingerprint at m/ by getting the xpub at m/0', which contains the parent fingerprint. + The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/. + """ + bb02 = self.init() + return bb02.root_fingerprint().hex() + + def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: + raise UnavailableActionError( + "The BitBox02 does not need a PIN sent from the host" + ) + + def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: + raise UnavailableActionError( + "The BitBox02 does not need a PIN sent from the host" + ) + + def _get_coin(self) -> bitbox02.btc.BTCCoin: + if self.is_testnet: + return bitbox02.btc.TBTC + return bitbox02.btc.BTC + + def _get_xpub(self, keypath: Sequence[int]) -> str: + xpub_type = ( + bitbox02.btc.BTCPubRequest.TPUB + if self.is_testnet + else bitbox02.btc.BTCPubRequest.XPUB + ) + return self.init().btc_xpub( + keypath, coin=self._get_coin(), xpub_type=xpub_type, display=False + ) + + def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: + path_uint32s = _parse_path(bip32_path) + try: + xpub = self._get_xpub(path_uint32s) + except Bitbox02Exception as exc: + raise BitBox02Error(str(exc)) + return {"xpub": xpub} + + @bitbox02_exception + def display_address( + self, + bip32_path: str, + p2sh_p2wpkh: bool, + bech32: bool, + redeem_script: Optional[str] = None, + descriptor: Optional[Descriptor] = None, + ) -> Dict[str, str]: + if redeem_script: + raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") + + if p2sh_p2wpkh: + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ) + elif bech32: + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ) + else: + raise UnavailableActionError( + "The BitBox02 does not support legacy p2pkh addresses" + ) + address = self.init().btc_address( + _parse_path(bip32_path), + coin=self._get_coin(), + script_config=script_config, + display=True, + ) + return {"address": address} + + @bitbox02_exception + def sign_tx(self, psbt: PSBT) -> Dict[str, str]: + def find_our_key( + keypaths: Dict[bytes, Sequence[int]] + ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: + """ + Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey. + Returns the pubkey and the keypath, without the fingerprint. + """ + for pubkey, keypath_with_fingerprint in keypaths.items(): + fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:] + # Cheap check if the key is ours. + if fp != master_fp: + continue + + # Expensive check if the key is ours. + # TODO: check for fingerprint collision + # keypath_account = keypath[:-2] + + return pubkey, keypath + return None, None + + def get_simple_type( + output: CTxOut, redeem_script: bytes + ) -> bitbox02.btc.BTCScriptConfig.SimpleType: + if is_p2pkh(output.scriptPubKey): + raise BadArgumentError( + "The BitBox02 does not support legacy p2pkh scripts" + ) + if is_p2wpkh(output.scriptPubKey): + return bitbox02.btc.BTCScriptConfig.P2WPKH + if output.is_p2sh() and is_p2wpkh(redeem_script): + return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + raise BadArgumentError( + "Input script type not recognized of input {}.".format(input_index) + ) + + master_fp = struct.unpack(" Dict[str, str]: + raise UnavailableActionError("The BitBox02 does not support 'signmessage'") + + @bitbox02_exception + def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: + bb02 = self.init() + info = bb02.device_info() + if info["mnemonic_passphrase_enabled"]: + bb02.disable_mnemonic_passphrase() + else: + bb02.enable_mnemonic_passphrase() + return {"success": True} + + @bitbox02_exception + def setup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + if passphrase: + raise UnavailableActionError( + "Passphrase not needed when setting up a BitBox02." + ) + + bb02 = self.init(expect_initialized=False) + + if label: + bb02.set_device_name(label) + if not bb02.set_password(): + return {"success": False} + return {"success": bb02.create_backup()} + + @bitbox02_exception + def wipe_device(self) -> Dict[str, Union[bool, str, int]]: + return {"success": self.init().reset()} + + @bitbox02_exception + def backup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + if label or passphrase: + raise UnavailableActionError( + "Label/passphrase not needed when exporting mnemonic from the BitBox02." + ) + + return {"success": self.init().show_mnemonic()} + + @bitbox02_exception + def restore_device( + self, label: str = "", word_count: int = 24 + ) -> Dict[str, Union[bool, str, int]]: + bb02 = self.init(expect_initialized=False) + + if label: + bb02.set_device_name(label) + + return {"success": bb02.restore_from_mnemonic()} diff --git a/src/cryptoadvance/specter/hwi_rpc.py b/src/cryptoadvance/specter/hwi_rpc.py index 14e323be3..1917c549b 100644 --- a/src/cryptoadvance/specter/hwi_rpc.py +++ b/src/cryptoadvance/specter/hwi_rpc.py @@ -7,6 +7,9 @@ from .util.json_rpc import JSONRPC import threading from .devices import __all__ as device_classes from contextlib import contextmanager +import logging + +logger = logging.getLogger(__name__) hwi_classes = [ cls for cls in device_classes if cls.hwi_support ] @@ -247,63 +250,103 @@ class HWIBridge(JSONRPC): # https://github.com/satoshilabs/slips/blob/master/slip-0132.md # Extract nested Segwit - xpub = client.get_pubkey_at_path( - 'm/49h/0h/{}h'.format(account) - )['xpub'] - ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') - xpubs += "[{}/49'/0'/{}']{}\n".format(master_fpr, account, ypub) + try: + xpub = client.get_pubkey_at_path( + 'm/49h/0h/{}h'.format(account) + )['xpub'] + ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') + xpubs += "[{}/49'/0'/{}']{}\n".format(master_fpr, account, ypub) + except Exception: + logger.warn( + "Failed to import Nested Segwit singlesig mainnet key." + ) - # native Segwit - xpub = client.get_pubkey_at_path( - 'm/84h/0h/{}h'.format(account) - )['xpub'] - zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') - xpubs += "[{}/84'/0'/{}']{}\n".format(master_fpr, account, zpub) + try: + # native Segwit + xpub = client.get_pubkey_at_path( + 'm/84h/0h/{}h'.format(account) + )['xpub'] + zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') + xpubs += "[{}/84'/0'/{}']{}\n".format(master_fpr, account, zpub) + except Exception: + logger.warn( + "Failed to import native Segwit singlesig mainnet key." + ) - # Multisig nested Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/0h/{}h/1h'.format(account) - )['xpub'] - Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') - xpubs += "[{}/48'/0'/{}'/1']{}\n".format(master_fpr, account, Ypub) + try: + # Multisig nested Segwit + xpub = client.get_pubkey_at_path( + 'm/48h/0h/{}h/1h'.format(account) + )['xpub'] + Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') + xpubs += "[{}/48'/0'/{}'/1']{}\n".format(master_fpr, account, Ypub) + except Exception: + logger.warn( + "Failed to import Nested Segwit multisig mainnet key." + ) - # Multisig native Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/0h/{}h/2h'.format(account) - )['xpub'] - Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') - xpubs += "[{}/48'/0'/{}'/2']{}\n".format(master_fpr, account, Zpub) + try: + # Multisig native Segwit + xpub = client.get_pubkey_at_path( + 'm/48h/0h/{}h/2h'.format(account) + )['xpub'] + Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') + xpubs += "[{}/48'/0'/{}'/2']{}\n".format(master_fpr, account, Zpub) + except Exception: + logger.warn( + "Failed to import native Segwit multisig mainnet key." + ) # And testnet client.is_testnet = True - # Testnet nested Segwit - xpub = client.get_pubkey_at_path( - 'm/49h/1h/{}h'.format(account) - )['xpub'] - upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') - xpubs += "[{}/49'/1'/{}']{}\n".format(master_fpr, account, upub) + try: + # Testnet nested Segwit + xpub = client.get_pubkey_at_path( + 'm/49h/1h/{}h'.format(account) + )['xpub'] + upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') + xpubs += "[{}/49'/1'/{}']{}\n".format(master_fpr, account, upub) + except Exception: + logger.warn( + "Failed to import Nested Segwit singlesig testnet key." + ) - # Testnet native Segwit - xpub = client.get_pubkey_at_path( - 'm/84h/1h/{}h'.format(account) - )['xpub'] - vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') - xpubs += "[{}/84'/1'/{}']{}\n".format(master_fpr, account, vpub) + try: + # Testnet native Segwit + xpub = client.get_pubkey_at_path( + 'm/84h/1h/{}h'.format(account) + )['xpub'] + vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') + xpubs += "[{}/84'/1'/{}']{}\n".format(master_fpr, account, vpub) + except Exception: + logger.warn( + "Failed to import native Segwit singlesig testnet key." + ) - # Testnet multisig nested Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/1h/{}h/1h'.format(account) - )['xpub'] - Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') - xpubs += "[{}/48'/1'/{}'/1']{}\n".format(master_fpr, account, Upub) + try: + # Testnet multisig nested Segwit + xpub = client.get_pubkey_at_path( + 'm/48h/1h/{}h/1h'.format(account) + )['xpub'] + Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') + xpubs += "[{}/48'/1'/{}'/1']{}\n".format(master_fpr, account, Upub) + except Exception: + logger.warn( + "Failed to import Nested Segwit multisigsig testnet key." + ) - # Testnet multisig native Segwit - xpub = client.get_pubkey_at_path( - 'm/48h/1h/{}h/2h'.format(account) - )['xpub'] - Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') - xpubs += "[{}/48'/1'/{}'/2']{}\n".format(master_fpr, account, Vpub) + try: + # Testnet multisig native Segwit + xpub = client.get_pubkey_at_path( + 'm/48h/1h/{}h/2h'.format(account) + )['xpub'] + Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') + xpubs += "[{}/48'/1'/{}'/2']{}\n".format(master_fpr, account, Vpub) + except Exception: + logger.warn( + "Failed to import native Segwit multisig testnet key." + ) # Do proper cleanup otherwise have to reconnect device to access again client.close() diff --git a/src/cryptoadvance/specter/static/hwi.js b/src/cryptoadvance/specter/static/hwi.js index f2614e06e..23d9af6cd 100644 --- a/src/cryptoadvance/specter/static/hwi.js +++ b/src/cryptoadvance/specter/static/hwi.js @@ -1,7 +1,7 @@ class HWIBridge { constructor(url, chain) { this.url = url; - this.deviceTypes = ['specter', 'coldcard', 'keepkey', 'ledger', 'trezor']; + this.deviceTypes = ['specter', 'coldcard', 'keepkey', 'ledger', 'bitbox02', 'trezor']; this.chain = chain; this.in_progress = false; } diff --git a/src/cryptoadvance/specter/static/img/bitbox02_icon.svg b/src/cryptoadvance/specter/static/img/bitbox02_icon.svg new file mode 100644 index 000000000..ebea2958c --- /dev/null +++ b/src/cryptoadvance/specter/static/img/bitbox02_icon.svg @@ -0,0 +1,39 @@ + +image/svg+xml + + + + + + + + + + + + + + diff --git a/src/cryptoadvance/specter/static/styles.css b/src/cryptoadvance/specter/static/styles.css index 861cceb5d..d03a79fbc 100644 --- a/src/cryptoadvance/specter/static/styles.css +++ b/src/cryptoadvance/specter/static/styles.css @@ -902,6 +902,10 @@ img[src="/static/img/cobo_icon.svg"] { transform: scale(1.30); } +img[src="/static/img/bitbox02_icon.svg"] { + transform: scale(2); +} + /************** Mobile styles ********************************/ #side-expand{ display: none; diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_device_list_item.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_device_list_item.jinja index 5c73c9d6f..9bc260d67 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_device_list_item.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_device_list_item.jinja @@ -6,7 +6,7 @@ #} {% macro sidebar_device_list_item(device, device_alias) -%} - {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo'] %} + {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo', 'bitbox02'] %} {% else %} diff --git a/src/cryptoadvance/specter/templates/wallet/new_wallet/import_wallet.jinja b/src/cryptoadvance/specter/templates/wallet/new_wallet/import_wallet.jinja index c03a070bf..40fcb8889 100644 --- a/src/cryptoadvance/specter/templates/wallet/new_wallet/import_wallet.jinja +++ b/src/cryptoadvance/specter/templates/wallet/new_wallet/import_wallet.jinja @@ -20,7 +20,7 @@
- {% if cosigner.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo'] %} + {% if cosigner.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo', 'bitbox02'] %} {% else %} diff --git a/src/cryptoadvance/specter/templates/wallet/new_wallet/new_wallet.jinja b/src/cryptoadvance/specter/templates/wallet/new_wallet/new_wallet.jinja index d547898b2..ec7f6cb74 100644 --- a/src/cryptoadvance/specter/templates/wallet/new_wallet/new_wallet.jinja +++ b/src/cryptoadvance/specter/templates/wallet/new_wallet/new_wallet.jinja @@ -40,12 +40,13 @@

Pick the device you want to use

{% endif %}
- {% for device_name in specter.device_manager.devices_names %} + {# Disable BitBox02 multisig for now... #} + {% for device_name in specter.device_manager.devices_names if wallet_type != 'multisig' or specter.device_manager.devices[device_name].device_type != 'bitbox02' %} {% set device = specter.device_manager.devices[device_name] %}
- {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo'] %} + {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo', 'bitbox02'] %} {% else %} diff --git a/src/cryptoadvance/specter/templates/wallet/settings/wallet_settings.jinja b/src/cryptoadvance/specter/templates/wallet/settings/wallet_settings.jinja index e74696803..b9818aaaf 100644 --- a/src/cryptoadvance/specter/templates/wallet/settings/wallet_settings.jinja +++ b/src/cryptoadvance/specter/templates/wallet/settings/wallet_settings.jinja @@ -101,7 +101,7 @@ {% for device in wallet.devices %}
- {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo'] %} + {% if device.device_type in ['specter', 'coldcard', 'trezor', 'ledger', 'cobo', 'bitbox02'] %} {% else %} diff --git a/src/cryptoadvance/specter/templates/wizards/singlesig_setup_wizard.jinja b/src/cryptoadvance/specter/templates/wizards/singlesig_setup_wizard.jinja index d8de5aaaf..694131a34 100644 --- a/src/cryptoadvance/specter/templates/wizards/singlesig_setup_wizard.jinja +++ b/src/cryptoadvance/specter/templates/wizards/singlesig_setup_wizard.jinja @@ -28,7 +28,7 @@