diff --git a/hwilib/devices/jade.py b/hwilib/devices/jade.py index bff69cd7c..2530ebb34 100644 --- a/hwilib/devices/jade.py +++ b/hwilib/devices/jade.py @@ -52,6 +52,7 @@ parse_multisig ) +import base64 import logging import semver import os @@ -90,6 +91,7 @@ def func(*args: Any, **kwargs: Any) -> Any: # This class extends the HardwareWalletClient for Blockstream Jade specific things class JadeClient(HardwareWalletClient): MIN_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 32) + PSBT_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 47) NETWORKS = {Chain.MAIN: 'mainnet', Chain.TEST: 'testnet', @@ -131,12 +133,12 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal self.jade.connect() verinfo = self.jade.get_version_info() + self.fw_version = semver.parse_version_info(verinfo['JADE_VERSION']) uninitialized = verinfo['JADE_STATE'] not in ['READY', 'TEMP'] # Check minimum supported firmware version (ignore candidate/build parts) - fw_version = semver.parse_version_info(verinfo['JADE_VERSION']) - if self.MIN_SUPPORTED_FW_VERSION > fw_version.finalize_version(): - raise DeviceNotReadyError(f'Jade fw version: {fw_version} - minimum required version: {self.MIN_SUPPORTED_FW_VERSION}. ' + if self.MIN_SUPPORTED_FW_VERSION > self.fw_version.finalize_version(): + raise DeviceNotReadyError(f'Jade fw version: {self.fw_version} - minimum required version: {self.MIN_SUPPORTED_FW_VERSION}. ' 'Please update using a Blockstream Green companion app') if path == SIMULATOR_PATH: if uninitialized: @@ -165,10 +167,9 @@ def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: ext_key = ExtendedKey.deserialize(xpub) return ext_key - # Walk the PSBT looking for inputs we can sign. Push any signatures into the - # 'partial_sigs' map in the input, and return the updated PSBT. - @jade_exception - def sign_tx(self, tx: PSBT) -> PSBT: + # Old firmware does not have native PSBT handling - walk the PSBT looking for inputs we can sign. + # Push any signatures into the 'partial_sigs' map in the input, and return the updated PSBT. + def legacy_sign_tx(self, tx: PSBT) -> PSBT: """ Sign a transaction with the Blockstream Jade. """ @@ -366,6 +367,29 @@ def _split_at_last_hardened_element(path: Sequence[int]) -> Tuple[Sequence[int], # Return the updated psbt return tx + # Sign tx PSBT - newer Jade firmware supports native PSBT signing, but old firmwares require + # mapping to the legacy 'sign_tx' structures. + @jade_exception + def sign_tx(self, tx: PSBT) -> PSBT: + """ + Sign a transaction with the Blockstream Jade. + """ + # Old firmware does not have native PSBT handling - use legacy method + if self.PSBT_SUPPORTED_FW_VERSION > self.fw_version.finalize_version(): + return self.legacy_sign_tx(tx) + + # Firmware 0.1.47 (March 2023) and later support native PSBT signing + psbt_b64 = tx.serialize() + psbt_bytes = base64.b64decode(psbt_b64.strip()) + + # NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979) + psbt_bytes = self.jade.sign_psbt(self._network(), psbt_bytes) + psbt_b64 = base64.b64encode(psbt_bytes).decode() + + psbt_signed = PSBT() + psbt_signed.deserialize(psbt_b64) + return psbt_signed + # Sign message, confirmed on device @jade_exception def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: diff --git a/test/test_jade.py b/test/test_jade.py index 710bf914f..1e707dfe2 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -215,6 +215,11 @@ def test_get_signing_p2shwsh(self): result = self.do_command(self.dev_args + ['displayaddress', descriptor_param]) self.assertEqual(result['address'], '2NAXBEePa5ebo1zTDrtQ9C21QDkkamwczfQ', result) +class TestJadeSignTx(TestSignTx): + # disable big psbt as jade simulator can't handle it + def test_big_tx(self): + pass + def jade_test_suite(emulator, bitcoind, interface): dev_emulator = JadeEmulator(emulator) @@ -234,7 +239,7 @@ def jade_test_suite(emulator, bitcoind, interface): suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestJadeGetMultisigAddresses, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases)) + suite.addTest(DeviceTestCase.parameterize(TestJadeSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) return result.wasSuccessful()