2020-02-18 15:31:05 +01:00
#
# BitBox02 Electrum plugin code.
#
import hid
2020-04-06 16:13:13 +02:00
from typing import TYPE_CHECKING , Dict , Tuple , Optional , List , Any , Callable
2020-02-18 15:31:05 +01:00
2024-10-10 19:24:27 +00:00
import electrum_ecc as ecc
2020-02-18 15:31:05 +01:00
from electrum import bip32 , constants
from electrum . i18n import _
2020-04-06 16:51:38 +02:00
from electrum . keystore import Hardware_KeyStore
2023-02-17 11:07:19 +00:00
from electrum . transaction import PartialTransaction , Sighash
2023-08-31 14:09:45 +02:00
from electrum . wallet import Multisig_Wallet , Deterministic_Wallet
2023-02-17 11:35:03 +00:00
from electrum . util import UserFacingException
2020-02-18 15:31:05 +01:00
from electrum . logging import get_logger
2020-09-08 15:52:53 +00:00
from electrum . plugin import Device , DeviceInfo , runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
from electrum . simple_config import SimpleConfig
from electrum . storage import get_derivation_used_for_hw_device_encryption
2020-04-06 17:23:22 +02:00
from electrum . bitcoin import OnchainOutputType
2020-02-18 15:31:05 +01:00
import electrum . bitcoin as bitcoin
2025-04-10 10:13:24 +02:00
from electrum . hw_wallet import HW_PluginBase , HardwareClientBase , HardwareHandlerBase
2020-02-18 15:31:05 +01:00
2023-08-22 18:14:47 +02:00
if TYPE_CHECKING :
from electrum . wizard import NewWalletWizard
2020-02-18 15:31:05 +01:00
2021-01-11 00:05:23 +01:00
_logger = get_logger ( __name__ )
2020-02-18 15:31:05 +01:00
try :
from bitbox02 import bitbox02
from bitbox02 import util
2023-09-11 10:39:08 +02:00
from bitbox02 . communication import ( devices , HARDENED , u2fhid , bitbox_api_protocol ,
FirmwareVersionOutdatedException )
2020-02-18 15:31:05 +01:00
requirements_ok = True
2021-01-11 00:05:23 +01:00
except ImportError as e :
if not ( isinstance ( e , ModuleNotFoundError ) and e . name == ' bitbox02 ' ) :
_logger . exception ( ' error importing bitbox02 plugin deps ' )
2020-02-18 15:31:05 +01:00
requirements_ok = False
2023-09-11 10:39:08 +02:00
class BitBox02NotInitialized ( UserFacingException ) :
pass
2020-02-18 15:31:05 +01:00
class BitBox02Client ( HardwareClientBase ) :
# handler is a BitBox02_Handler, importing it would lead to a circular dependency
2022-05-11 19:13:00 +02:00
def __init__ ( self , handler : HardwareHandlerBase , device : Device , config : SimpleConfig , * , plugin : HW_PluginBase ) :
2020-04-17 19:25:18 +02:00
HardwareClientBase . __init__ ( self , plugin = plugin )
2020-04-17 19:05:56 +02:00
self . bitbox02_device = None # type: Optional[bitbox02.BitBox02]
2020-02-18 15:31:05 +01:00
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 " )
2023-09-06 12:36:16 +02:00
def device_model_name ( self ) - > Optional [ str ] :
return ' BitBox02 '
2020-02-18 15:31:05 +01:00
def is_initialized ( self ) - > bool :
return True
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
def close ( self ) :
2020-09-08 15:52:53 +00:00
try :
self . bitbox02_device . close ( )
2023-04-23 01:33:12 +00:00
except Exception :
2020-09-08 15:52:53 +00:00
pass
2020-02-18 15:31:05 +01:00
def has_usable_connection_with_device ( self ) - > bool :
if self . bitbox_hid_info is None :
2020-04-06 16:13:13 +02:00
return False
2020-02-18 15:31:05 +01:00
return True
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-07-14 16:44:29 +02:00
def get_soft_device_id ( self ) - > Optional [ str ] :
if self . handler is None :
# Can't do the pairing without the handler. This happens at wallet creation time, when
# listing the devices.
return None
if self . bitbox02_device is None :
2020-07-14 17:04:21 +02:00
self . pairing_dialog ( )
2020-07-14 16:44:29 +02:00
return self . bitbox02_device . root_fingerprint ( ) . hex ( )
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-07-14 17:04:21 +02:00
def pairing_dialog ( self ) :
2020-04-06 16:13:13 +02:00
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 ( )
2023-04-23 01:33:12 +00:00
except Exception :
2020-04-06 16:13:13 +02:00
# Close the hid device on exception
2020-09-08 15:52:53 +00:00
hid_device . close ( )
2020-04-06 16:13:13 +02:00
raise
finally :
self . handler . finished ( )
return res
2020-02-18 15:31:05 +01:00
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 :
2023-09-11 10:39:08 +02:00
if pubkey . hex ( ) in noise_keys :
2020-02-18 15:31:05 +01:00
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 :
2020-04-06 16:21:09 +02:00
self . handler . show_error (
" The BitBox02 attestation failed. \n Try reconnecting the BitBox02. \n Warning: The device might not be genuine, if the \n problem persists please contact Shift support. " ,
blocking = True
2020-02-18 15:31:05 +01:00
)
class NoiseConfig ( bitbox_api_protocol . BitBoxNoiseConfig ) :
""" NoiseConfig extends BitBoxNoiseConfig """
2020-04-06 16:13:13 +02:00
def show_pairing ( self , code : str , device_response : Callable [ [ ] , bool ] ) - > bool :
return pairing_step ( code , device_response )
2020-02-18 15:31:05 +01:00
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 :
2020-09-08 15:52:53 +00:00
hid_device = hid . device ( )
hid_device . open_path ( self . bitbox_hid_info [ " path " ] )
2020-06-03 15:33:45 +02:00
bitbox02_device = bitbox02 . BitBox02 (
2020-02-18 15:31:05 +01:00
transport = u2fhid . U2FHid ( hid_device ) ,
device_info = self . bitbox_hid_info ,
noise_config = NoiseConfig ( ) ,
)
2020-06-03 15:33:45 +02:00
try :
bitbox02_device . check_min_version ( )
except FirmwareVersionOutdatedException :
raise
self . bitbox02_device = bitbox02_device
2020-02-18 15:31:05 +01:00
2020-04-06 16:13:13 +02:00
self . fail_if_not_initialized ( )
def fail_if_not_initialized ( self ) - > None :
assert self . bitbox02_device
2020-02-18 15:31:05 +01:00
if not self . bitbox02_device . device_info ( ) [ " initialized " ] :
2023-09-11 10:39:08 +02:00
raise BitBox02NotInitialized (
2020-02-18 15:31:05 +01:00
" Please initialize the BitBox02 using the BitBox app first before using the BitBox02 in electrum "
)
def coin_network_from_electrum_network ( self ) - > int :
if constants . net . TESTNET :
return bitbox02 . btc . TBTC
return bitbox02 . btc . BTC
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
def get_password_for_storage_encryption ( self ) - > str :
2024-03-21 21:32:42 +01:00
if self . bitbox02_device is None :
self . pairing_dialog ( )
if self . bitbox02_device is None :
raise Exception (
" Need to setup communication first before attempting any BitBox02 calls "
)
2020-02-18 15:31:05 +01:00
derivation = get_derivation_used_for_hw_device_encryption ( )
2023-03-10 14:23:17 +00:00
derivation_list = bip32 . convert_bip32_strpath_to_intpath ( derivation )
2020-02-18 15:31:05 +01:00
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 ( )
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-04-06 16:49:11 +02:00
def get_xpub ( self , bip32_path : str , xtype : str , * , display : bool = False ) - > str :
2020-02-18 15:31:05 +01:00
if self . bitbox02_device is None :
2020-07-14 17:04:21 +02:00
self . pairing_dialog ( )
2020-02-18 15:31:05 +01:00
if self . bitbox02_device is None :
raise Exception (
" Need to setup communication first before attempting any BitBox02 calls "
)
2020-04-06 16:13:13 +02:00
self . fail_if_not_initialized ( )
2020-02-18 15:31:05 +01:00
2023-03-10 14:23:17 +00:00
xpub_keypath = bip32 . convert_bip32_strpath_to_intpath ( bip32_path )
2020-02-18 15:31:05 +01:00
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
2020-10-07 11:16:50 +02:00
elif xtype == " p2wsh-p2sh " :
if coin_network == bitbox02 . btc . BTC :
out_type = bitbox02 . btc . BTCPubRequest . CAPITAL_YPUB
else :
out_type = bitbox02 . btc . BTCPubRequest . CAPITAL_UPUB
2020-02-18 15:31:05 +01:00
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 ) )
2023-09-11 10:39:08 +02:00
return self . bitbox02_device . btc_xpub ( keypath = xpub_keypath , xpub_type = out_type , coin = coin_network ,
display = display )
2020-02-18 15:31:05 +01:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-07-15 12:59:48 +02:00
def label ( self ) - > str :
if self . handler is None :
# Can't do the pairing without the handler. This happens at wallet creation time, when
# listing the devices.
return super ( ) . label ( )
if self . bitbox02_device is None :
self . pairing_dialog ( )
2020-07-15 13:13:03 +02:00
# We add the fingerprint to the label, as if there are two devices with the same label, the
# device manager can mistake one for another and fail.
return " %s ( %s ) " % (
self . bitbox02_device . device_info ( ) [ " name " ] ,
self . bitbox02_device . root_fingerprint ( ) . hex ( ) ,
)
2020-07-15 12:59:48 +02:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
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
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
def btc_multisig_config (
2020-10-07 11:16:50 +02:00
self , coin , bip32_path : List [ int ] , wallet : Multisig_Wallet , xtype : str ,
2020-02-18 15:31:05 +01:00
) :
"""
Set and get a multisig config with the current device and some other arbitrary xpubs .
Registers it on the device if not already registered .
2020-10-07 11:16:50 +02:00
xtype : ' p2wsh ' | ' p2wsh-p2sh '
2020-02-18 15:31:05 +01:00
"""
2020-10-07 11:16:50 +02:00
assert xtype in ( " p2wsh " , " p2wsh-p2sh " )
2020-02-18 15:31:05 +01:00
if self . bitbox02_device is None :
raise Exception (
" Need to setup communication first before attempting any BitBox02 calls "
)
2020-11-23 14:28:28 +01:00
account_keypath = bip32_path [ : - 2 ]
2020-02-18 15:31:05 +01:00
xpubs = wallet . get_master_public_keys ( )
our_xpub = self . get_xpub (
2020-10-07 11:16:50 +02:00
bip32 . convert_bip32_intpath_to_strpath ( account_keypath ) , xtype
2020-02-18 15:31:05 +01:00
)
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 ) ,
2020-10-07 11:16:50 +02:00
script_type = {
" p2wsh " : bitbox02 . btc . BTCScriptConfig . Multisig . P2WSH ,
" p2wsh-p2sh " : bitbox02 . btc . BTCScriptConfig . Multisig . P2WSH_P2SH ,
} [ xtype ]
2020-02-18 15:31:05 +01:00
)
)
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
2023-04-23 01:33:12 +00:00
except Exception :
2024-01-16 16:25:33 +01:00
raise UserFacingException ( _ ( ' Failed to register multisig \n account configuration on BitBox02 ' ) )
2020-02-18 15:31:05 +01:00
return multisig_config
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
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 "
)
2023-03-10 14:23:17 +00:00
address_keypath = bip32 . convert_bip32_strpath_to_intpath ( bip32_path )
2020-02-18 15:31:05 +01:00
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
)
2020-10-07 11:16:50 +02:00
elif address_type in ( " p2wsh-p2sh " , " p2wsh " ) :
2020-02-18 15:31:05 +01:00
if type ( wallet ) is Multisig_Wallet :
script_config = self . btc_multisig_config (
2020-10-07 11:16:50 +02:00
coin_network , address_keypath , wallet , address_type ,
2020-02-18 15:31:05 +01:00
)
else :
2020-10-07 11:16:50 +02:00
raise Exception ( " Can only use p2wsh-p2sh or p2wsh with multisig wallets " )
2020-02-18 15:31:05 +01:00
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 ,
)
2020-10-07 11:49:37 +02:00
def _get_coin ( self ) :
return bitbox02 . btc . TBTC if constants . net . TESTNET else bitbox02 . btc . BTC
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
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 "
)
2020-10-07 11:49:37 +02:00
coin = self . _get_coin ( )
2020-02-18 15:31:05 +01:00
tx_script_type = None
# Build BTCInputType list
inputs = [ ]
for txin in tx . inputs ( ) :
2020-06-04 19:41:34 +02:00
my_pubkey , full_path = keystore . find_my_pubkey_in_txinout ( txin )
2020-02-18 15:31:05 +01:00
if full_path is None :
raise Exception (
" A wallet owned pubkey was not found in the transaction input to be signed "
)
2020-06-04 19:41:34 +02:00
prev_tx = txin . utxo
2020-06-03 15:12:49 +02:00
if prev_tx is None :
2020-06-04 19:41:34 +02:00
raise UserFacingException ( _ ( ' Missing previous tx. ' ) )
2020-06-03 15:12:49 +02:00
prev_inputs : List [ bitbox02 . BTCPrevTxInputType ] = [ ]
prev_outputs : List [ bitbox02 . BTCPrevTxOutputType ] = [ ]
for prev_txin in prev_tx . inputs ( ) :
prev_inputs . append (
{
" prev_out_hash " : prev_txin . prevout . txid [ : : - 1 ] ,
" prev_out_index " : prev_txin . prevout . out_idx ,
" signature_script " : prev_txin . script_sig ,
" sequence " : prev_txin . nsequence ,
}
)
for prev_txout in prev_tx . outputs ( ) :
prev_outputs . append (
{
" value " : prev_txout . value ,
" pubkey_script " : prev_txout . scriptpubkey ,
}
)
2020-02-18 15:31:05 +01:00
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 ,
2020-06-29 14:24:09 +02:00
" script_config_index " : 0 ,
2020-06-03 15:12:49 +02:00
" prev_tx " : {
" version " : prev_tx . version ,
" locktime " : prev_tx . locktime ,
" inputs " : prev_inputs ,
" outputs " : prev_outputs ,
} ,
2020-02-18 15:31:05 +01:00
}
)
2023-04-20 15:17:36 +00:00
desc = txin . script_descriptor
assert desc
2022-05-24 02:39:11 +00:00
if tx_script_type is None :
2023-02-26 13:28:31 +00:00
tx_script_type = desc . to_legacy_electrum_script_type ( )
elif tx_script_type != desc . to_legacy_electrum_script_type ( ) :
2020-02-18 15:31:05 +01:00
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
)
2020-10-07 11:16:50 +02:00
elif tx_script_type in ( " p2wsh-p2sh " , " p2wsh " ) :
2020-02-18 15:31:05 +01:00
if type ( wallet ) is Multisig_Wallet :
2020-10-07 11:16:50 +02:00
tx_script_type = self . btc_multisig_config ( coin , full_path , wallet , tx_script_type )
2020-02-18 15:31:05 +01:00
else :
2020-10-07 11:16:50 +02:00
raise Exception ( " Can only use p2wsh-p2sh or p2wsh with multisig wallets " )
2020-02-18 15:31:05 +01:00
else :
raise UserFacingException (
2024-01-16 16:25:33 +01:00
_ ( ' Invalid input script type: {} is not supported by the BitBox02 ' ) . format ( tx_script_type )
2020-02-18 15:31:05 +01:00
)
# Build BTCOutputType list
outputs = [ ]
for txout in tx . outputs ( ) :
assert txout . address
# check for change
if txout . is_change :
2020-06-04 19:41:34 +02:00
my_pubkey , change_pubkey_path = keystore . find_my_pubkey_in_txinout ( txout )
2020-02-18 15:31:05 +01:00
outputs . append (
bitbox02 . BTCOutputInternal (
2020-06-29 14:24:09 +02:00
keypath = change_pubkey_path , value = txout . value , script_config_index = 0 ,
2020-02-18 15:31:05 +01:00
)
)
else :
2022-03-02 14:14:24 +01:00
addrtype , payload = bitcoin . address_to_payload ( txout . address )
2020-04-06 17:23:22 +02:00
if addrtype == OnchainOutputType . P2PKH :
2020-02-18 15:31:05 +01:00
output_type = bitbox02 . btc . P2PKH
2020-04-06 17:23:22 +02:00
elif addrtype == OnchainOutputType . P2SH :
2020-02-18 15:31:05 +01:00
output_type = bitbox02 . btc . P2SH
2020-04-06 17:23:22 +02:00
elif addrtype == OnchainOutputType . WITVER0_P2WPKH :
2020-02-18 15:31:05 +01:00
output_type = bitbox02 . btc . P2WPKH
2020-04-06 17:23:22 +02:00
elif addrtype == OnchainOutputType . WITVER0_P2WSH :
2020-02-18 15:31:05 +01:00
output_type = bitbox02 . btc . P2WSH
2022-03-02 14:14:24 +01:00
elif addrtype == OnchainOutputType . WITVER1_P2TR :
output_type = bitbox02 . btc . P2TR
2020-02-18 15:31:05 +01:00
else :
raise UserFacingException (
2024-01-16 16:25:33 +01:00
_ ( ' Received unsupported output type during transaction signing: {} is not supported by the BitBox02 ' ) . format (
2020-02-18 15:31:05 +01:00
addrtype
)
)
outputs . append (
bitbox02 . BTCOutputExternal (
output_type = output_type ,
2022-03-02 14:14:24 +01:00
output_payload = payload ,
2020-02-18 15:31:05 +01:00
value = txout . value ,
)
)
2020-11-23 14:28:28 +01:00
keypath_account = full_path [ : - 2 ]
2023-05-30 23:56:27 +02:00
format_unit = bitbox02 . btc . BTCSignInitRequest . FormatUnit . DEFAULT
# Base unit is configured to be "sat":
if self . config . get_decimal_point ( ) == 0 :
format_unit = bitbox02 . btc . BTCSignInitRequest . FormatUnit . SAT
2020-02-18 15:31:05 +01:00
sigs = self . bitbox02_device . btc_sign (
coin ,
2020-06-29 14:24:09 +02:00
[ bitbox02 . btc . BTCScriptConfigWithKeypath (
script_config = tx_script_type ,
keypath = keypath_account ,
) ] ,
2020-02-18 15:31:05 +01:00
inputs = inputs ,
outputs = outputs ,
locktime = tx . locktime ,
version = tx . version ,
2023-05-30 23:56:27 +02:00
format_unit = format_unit ,
2020-02-18 15:31:05 +01:00
)
# Fill signatures
if len ( sigs ) != len ( tx . inputs ( ) ) :
raise Exception ( " Incorrect number of inputs signed. " ) # Should never occur
2024-04-26 20:09:00 +00:00
sighash = Sighash . to_sigbytes ( Sighash . ALL )
signatures = [ ecc . ecdsa_der_sig_from_ecdsa_sig64 ( x [ 1 ] ) + sighash for x in sigs ]
2020-02-18 15:31:05 +01:00
tx . update_signatures ( signatures )
2022-02-22 19:20:03 +01:00
def sign_message ( self , keypath : str , message : bytes , script_type : str ) - > bytes :
2020-10-07 11:49:37 +02:00
if self . bitbox02_device is None :
raise Exception (
" Need to setup communication first before attempting any BitBox02 calls "
)
try :
simple_type = {
" p2wpkh-p2sh " : bitbox02 . btc . BTCScriptConfig . P2WPKH_P2SH ,
" p2wpkh " : bitbox02 . btc . BTCScriptConfig . P2WPKH ,
2022-02-22 19:20:03 +01:00
} [ script_type ]
2020-10-07 11:49:37 +02:00
except KeyError :
2024-01-16 16:25:33 +01:00
raise UserFacingException ( _ ( ' The BitBox02 does not support signing messages for this address type: {} ' ) . format ( script_type ) )
2020-10-07 11:49:37 +02:00
2024-01-16 16:25:33 +01:00
_a , _b , signature = self . bitbox02_device . btc_sign_msg (
2020-10-07 11:49:37 +02:00
self . _get_coin ( ) ,
bitbox02 . btc . BTCScriptConfigWithKeypath (
script_config = bitbox02 . btc . BTCScriptConfig (
simple_type = simple_type ,
) ,
2023-03-10 14:23:17 +00:00
keypath = bip32 . convert_bip32_strpath_to_intpath ( keypath ) ,
2020-10-07 11:49:37 +02:00
) ,
message ,
)
return signature
2020-02-18 15:31:05 +01:00
2023-09-11 10:39:08 +02:00
2020-02-18 15:31:05 +01:00
class BitBox02_KeyStore ( Hardware_KeyStore ) :
hw_type = " bitbox02 "
device = " BitBox02 "
plugin : " BitBox02Plugin "
2020-09-04 16:11:01 +02:00
def __init__ ( self , d : dict ) :
2020-02-18 15:31:05 +01:00
super ( ) . __init__ ( d )
self . ux_busy = False
2022-05-11 19:30:14 +02:00
def give_error ( self , message : Exception ) :
2020-02-18 15:31:05 +01:00
self . logger . info ( message )
if not self . ux_busy :
self . handler . show_error ( message )
else :
self . ux_busy = False
raise UserFacingException ( message )
def decrypt_message ( self , pubkey , message , password ) :
raise UserFacingException (
2024-01-16 16:25:33 +01:00
_ ( ' Message encryption, decryption and signing are currently not supported for {} ' ) . format ( self . device )
2020-02-18 15:31:05 +01:00
)
2022-02-22 19:20:03 +01:00
def sign_message ( self , sequence , message , password , * , script_type = None ) :
2022-12-19 07:49:40 +00:00
if constants . net . TESTNET :
raise UserFacingException (
_ ( " The {} only supports message signing on mainnet. " ) . format ( self . device )
)
2020-10-07 11:49:37 +02:00
if password :
raise Exception ( " BitBox02 does not accept a password from the host " )
client = self . get_client ( )
keypath = self . get_derivation_prefix ( ) + " / %d / %d " % sequence
2022-02-22 19:20:03 +01:00
return client . sign_message ( keypath , message . encode ( " utf-8 " ) , script_type )
2020-10-07 11:49:37 +02:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
def sign_transaction ( self , tx : PartialTransaction , password : str ) :
if tx . is_complete ( ) :
return
client = self . get_client ( )
2020-04-06 16:13:13 +02:00
assert isinstance ( client , BitBox02Client )
2020-02-18 15:31:05 +01:00
try :
try :
self . handler . show_message ( " Authorize Transaction... " )
2020-04-06 16:13:13 +02:00
client . sign_transaction ( self , tx , self . handler . get_wallet ( ) )
2020-02-18 15:31:05 +01:00
finally :
self . handler . finished ( )
except Exception as e :
self . logger . exception ( " " )
2022-05-11 19:30:14 +02:00
self . give_error ( e )
2020-02-18 15:31:05 +01:00
return
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
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 )
2023-09-11 10:39:08 +02:00
2020-02-18 15:31:05 +01:00
class BitBox02Plugin ( HW_PluginBase ) :
keystore_class = BitBox02_KeyStore
2025-07-17 12:07:24 +02:00
minimum_library = ( 7 , 0 , 0 )
2020-02-18 15:31:05 +01:00
DEVICE_IDS = [ ( 0x03EB , 0x2403 ) ]
2020-10-07 11:16:50 +02:00
SUPPORTED_XTYPES = ( " p2wpkh-p2sh " , " p2wpkh " , " p2wsh " , " p2wsh-p2sh " )
2020-02-18 15:31:05 +01:00
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
2020-04-06 18:20:21 +02:00
self . device_manager ( ) . register_devices ( self . DEVICE_IDS , plugin = self )
2020-02-18 15:31:05 +01:00
def get_library_version ( self ) :
try :
from bitbox02 import bitbox02
version = bitbox02 . __version__
2023-04-23 01:33:12 +00:00
except Exception :
2020-02-18 15:31:05 +01:00
version = " unknown "
if requirements_ok :
return version
else :
raise ImportError ( )
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2022-05-11 19:13:00 +02:00
def create_client ( self , device , handler ) - > BitBox02Client :
2020-04-17 19:25:18 +02:00
return BitBox02Client ( handler , device , self . config , plugin = self )
2020-02-18 15:31:05 +01:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-02-18 15:31:05 +01:00
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 )
2020-04-06 16:49:11 +02:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-04-06 16:49:11 +02:00
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 )
2020-04-06 18:20:21 +02:00
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_ )
2023-08-22 18:14:47 +02:00
def wizard_entry_for_device ( self , device_info : ' DeviceInfo ' , * , new_wallet = True ) - > str :
2023-09-11 10:39:08 +02:00
# Note: device_info.initialized for this hardware doesn't imply a seed is present,
# only that it has firmware installed
2023-08-22 18:14:47 +02:00
if new_wallet :
2023-08-23 12:03:30 +02:00
return ' bitbox02_start ' if device_info . initialized else ' bitbox02_not_initialized '
2023-08-22 18:14:47 +02:00
else :
2023-08-23 12:03:30 +02:00
return ' bitbox02_unlock '
2023-08-22 18:14:47 +02:00
2023-08-23 12:30:20 +02:00
# insert bitbox02 pages in new wallet wizard
2023-08-22 18:14:47 +02:00
def extend_wizard ( self , wizard : ' NewWalletWizard ' ) :
views = {
2023-08-23 12:03:30 +02:00
' bitbox02_start ' : {
' next ' : ' bitbox02_xpub ' ,
2023-08-22 18:14:47 +02:00
} ,
2023-08-23 12:03:30 +02:00
' bitbox02_xpub ' : {
2023-08-22 18:14:47 +02:00
' next ' : lambda d : wizard . wallet_password_view ( d ) if wizard . last_cosigner ( d ) else ' multisig_cosigner_keystore ' ,
' accept ' : wizard . maybe_master_pubkey ,
' last ' : lambda d : wizard . is_single_password ( ) and wizard . last_cosigner ( d )
} ,
2023-08-23 12:03:30 +02:00
' bitbox02_not_initialized ' : { } ,
' bitbox02_unlock ' : {
2023-08-22 18:14:47 +02:00
' last ' : True
} ,
}
wizard . navmap_merge ( views )