From be9e4937b8aa05d4682d640db08a2345f53368cc Mon Sep 17 00:00:00 2001 From: Jean Do Date: Tue, 16 Jul 2024 07:23:14 -0400 Subject: [PATCH 01/14] Directories suffixed with "/" in file_manager --- src/krux/pages/file_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/krux/pages/file_manager.py b/src/krux/pages/file_manager.py index 61aa2ac2c..3f788d411 100644 --- a/src/krux/pages/file_manager.py +++ b/src/krux/pages/file_manager.py @@ -61,7 +61,7 @@ def select_file( if path != SD_ROOT_PATH: items.append("..") - menu_items.append(("..", lambda: MENU_EXIT)) + menu_items.append(("../", lambda: MENU_EXIT)) # sorts by name ignorecase dir_files = sorted(os.listdir(path), key=str.lower) @@ -97,7 +97,10 @@ def select_file( or SDHandler.dir_exists(path + "/" + filename) ): items.append(filename) - display_filename = filename + display_filename = ( + filename + "/" if filename in directories else filename + ) + if len(filename) >= custom_start_digits + 2 + custom_end_digts: display_filename = ( filename[:custom_start_digits] From 7be968a6d607672a8592e66387837c707e75c9ab Mon Sep 17 00:00:00 2001 From: Jean Do Date: Tue, 16 Jul 2024 12:11:38 -0400 Subject: [PATCH 02/14] is_directory set w/ directories+files, one less os.stat, one less list search --- src/krux/pages/file_manager.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/krux/pages/file_manager.py b/src/krux/pages/file_manager.py index 3f788d411..c13de4f30 100644 --- a/src/krux/pages/file_manager.py +++ b/src/krux/pages/file_manager.py @@ -78,8 +78,11 @@ def select_file( del dir_files - # show sorted folders first than sorted files - for filename in directories + files: + # show sorted folders first then sorted files + for filename, is_directory in [(x, True) for x in directories] + [ + (x, False) for x in files + ]: + extension_match = False if isinstance(file_extension, str): # No extension filter or matches @@ -91,15 +94,9 @@ def select_file( extension_match = True break - if ( - extension_match - # Is a directory - or SDHandler.dir_exists(path + "/" + filename) - ): + if extension_match or is_directory: items.append(filename) - display_filename = ( - filename + "/" if filename in directories else filename - ) + display_filename = filename + "/" if is_directory else filename if len(filename) >= custom_start_digits + 2 + custom_end_digts: display_filename = ( From 7a4bee4efadf9dc5d8afebd60aac4d4a613baa95 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Wed, 17 Jul 2024 18:09:29 -0400 Subject: [PATCH 03/14] uses a more performant iterator per tadeubas --- src/krux/pages/file_manager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/krux/pages/file_manager.py b/src/krux/pages/file_manager.py index c13de4f30..b44ed4e09 100644 --- a/src/krux/pages/file_manager.py +++ b/src/krux/pages/file_manager.py @@ -79,9 +79,8 @@ def select_file( del dir_files # show sorted folders first then sorted files - for filename, is_directory in [(x, True) for x in directories] + [ - (x, False) for x in files - ]: + for i, filename in enumerate(directories + files): + is_directory = i < len(directories) extension_match = False if isinstance(file_extension, str): From afd1ddf4fe55993487539801a4b846a6e21270ee Mon Sep 17 00:00:00 2001 From: Jean Do Date: Thu, 18 Jul 2024 07:28:55 -0400 Subject: [PATCH 04/14] bugfix (ty tadeubas) for long directory filenames --- src/krux/pages/file_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/krux/pages/file_manager.py b/src/krux/pages/file_manager.py index b44ed4e09..5686ed352 100644 --- a/src/krux/pages/file_manager.py +++ b/src/krux/pages/file_manager.py @@ -99,9 +99,9 @@ def select_file( if len(filename) >= custom_start_digits + 2 + custom_end_digts: display_filename = ( - filename[:custom_start_digits] + display_filename[:custom_start_digits] + ".." - + filename[len(filename) - custom_end_digts :] + + display_filename[len(filename) - custom_end_digts :] ) menu_items.append( ( From 8c9b0583a07dafd922917f2e3ac878339390a864 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 19 Jul 2024 04:13:18 -0400 Subject: [PATCH 05/14] bugfixes 1 bugfix; adds 1 test, adjusts 2 others --- src/krux/pages/file_manager.py | 9 ++++-- tests/pages/test_file_manager.py | 51 +++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/krux/pages/file_manager.py b/src/krux/pages/file_manager.py index 5686ed352..6248fab89 100644 --- a/src/krux/pages/file_manager.py +++ b/src/krux/pages/file_manager.py @@ -97,11 +97,16 @@ def select_file( items.append(filename) display_filename = filename + "/" if is_directory else filename - if len(filename) >= custom_start_digits + 2 + custom_end_digts: + if ( + len(display_filename) + >= custom_start_digits + 2 + custom_end_digts + ): display_filename = ( display_filename[:custom_start_digits] + ".." - + display_filename[len(filename) - custom_end_digts :] + + display_filename[ + len(display_filename) - custom_end_digts : + ] ) menu_items.append( ( diff --git a/tests/pages/test_file_manager.py b/tests/pages/test_file_manager.py index a3758d844..9e8d263ba 100644 --- a/tests/pages/test_file_manager.py +++ b/tests/pages/test_file_manager.py @@ -6,7 +6,14 @@ def mock_file_operations(mocker): mocker.patch( "os.listdir", - new=mocker.MagicMock(return_value=["first_file", "second_file"]), + new=mocker.MagicMock( + return_value=[ + "file1", # third entry + "file2_has_a_long_name", # fourth entry + "subdir2", # second entry + "subdir1_has_a_long_name", # first entry + ] + ), ) mocker.patch("builtins.open", mocker.mock_open(read_data="")) mocker.patch("os.remove", mocker.mock_open(read_data="")) @@ -18,20 +25,30 @@ def test_file_exploring(m5stickv, mocker, mock_file_operations): import time BTN_SEQUENCE = ( - [BUTTON_PAGE] # Move to second file + [BUTTON_PAGE] # move to second entry, last directory + + [BUTTON_PAGE] # move to third entry, first file + + [BUTTON_PAGE] # move to fourth entry, last file + [BUTTON_ENTER] # Check file details + [BUTTON_ENTER] # Leave file details - + [BUTTON_PAGE] # Go to "back" + + [BUTTON_PAGE] # move to Back + [BUTTON_ENTER] # Leave file explorer ) def mock_localtime(timestamp): return time.gmtime(timestamp) + # selected file has this timestamp mocker.patch("time.localtime", side_effect=mock_localtime) + + # to view this directory, selected file isn't a directory mocker.patch( "krux.sd_card.SDHandler.dir_exists", mocker.MagicMock(side_effect=[True, False]) ) + # first 2 entries are files, next 2 are directories + mocker.patch( + "krux.sd_card.SDHandler.file_exists", + mocker.MagicMock(side_effect=[True, True, False, False]), + ) ctx = create_ctx(mocker, BTN_SEQUENCE) file_manager = FileManager(ctx) file_manager.select_file(select_file_handler=file_manager.show_file_details) @@ -41,12 +58,38 @@ def mock_localtime(timestamp): ctx.display.draw_hcentered_text.assert_has_calls( [ mocker.call( - "second_file\n\nSize: 1.1 KB\n\nCreated: 1970-01-01 00:00\n\nModified: 1970-01-01 00:00" + "file2_has_a_long_name\n\nSize: 1.1 KB\n\nCreated: 1970-01-01 00:00\n\nModified: 1970-01-01 00:00" ) ] ) +def test_files_and_folders_with_long_filenames(m5stickv, mocker, mock_file_operations): + from krux.pages.file_manager import FileManager + from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV + + # to view this directory + mocker.patch( + "krux.sd_card.SDHandler.dir_exists", mocker.MagicMock(side_effect=[True]) + ) + # first 2 entries are files, next 2 are directories + mocker.patch( + "krux.sd_card.SDHandler.file_exists", + mocker.MagicMock(side_effect=[True, True, False, False]), + ) + ctx = create_ctx(mocker, ([BUTTON_PAGE_PREV, BUTTON_ENTER])) # to back and out + file_manager = FileManager(ctx) + file_manager.select_file() + ctx.display.to_lines.assert_has_calls( + [ + mocker.call("subdi..ong_name/"), + mocker.call("subdir2/"), + mocker.call("file1"), + mocker.call("file2..long_name"), + ] + ) + + def test_folders_exploring(m5stickv, mocker, mock_file_operations): from krux.pages.file_manager import FileManager from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV From ca61e0d8f763fe10d370f2cac60a029656b1a2fa Mon Sep 17 00:00:00 2001 From: Jean Do Date: Sun, 21 Jul 2024 08:02:37 -0400 Subject: [PATCH 06/14] also tries to get descriptor from qr:urtype.Account --- src/krux/wallet.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/krux/wallet.py b/src/krux/wallet.py index 7b1fe74e3..367c7d20b 100644 --- a/src/krux/wallet.py +++ b/src/krux/wallet.py @@ -207,6 +207,15 @@ def parse_wallet(wallet_data, allow_assumption=None): except: pass + # Try to parse as a Crypto-Account type + try: + account = urtypes.crypto.Account.from_cbor( + wallet_data.cbor + ).output_descriptors[0] + return Descriptor.from_string(account.descriptor()), None + except: + pass + # Treat the UR as a generic UR bytes object and extract the data for further processing wallet_data = urtypes.Bytes.from_cbor(wallet_data.cbor).data From 16b6d28e8a177f5b2c7056d5ff0b5444d8d72928 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Tue, 23 Jul 2024 16:36:11 -0400 Subject: [PATCH 07/14] tests for wallet descriptor via UR:Account/Output --- tests/test_wallet.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 25389cad4..c5b0c1358 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,4 +1,5 @@ import pytest +from ur.ur_decoder import URDecoder @pytest.fixture @@ -979,3 +980,59 @@ def test_derivation_to_script_wrapper(): for _case in cases: assert derivation_to_script_wrapper(_case[0]) == _case[1] + + +def test_parse_wallet_via_ur_output(mocker, m5stickv): + from krux.wallet import parse_wallet + + # sparrow uses this ur format + QRDATA = [ + # testnet legacy p2pkh + "UR:CRYPTO-OUTPUT/TAADMUTAADDLOSAOWKAXHDCLAXJPDPECBNTOVEKOVOADIHWDDIBKHTHYAECWBSWPDEYTMSLEDRBSPMAMDEVAURWFDWAAHDCXKKSGMELBWMKOFRDMOEDAUEMUQDVDVANEBGVOMYGLKOVSFDSKDEJYKOGDGTINETTOAHTAADEHOEADAEAOADAMTAADDYOTADLNCSDWYKADYKAEYKAOCYTBFSSSOSAXAXAYCYLBZMISPFASIMGUIHIHIEGUINIOJTIHJPMEZMDWFY", + # testnet nested segwit + "UR:CRYPTO-OUTPUT/TAADMHTAADMWTAADDLOSAOWKAXHDCLAXPKGWJSLTINFRRELGGHECCMJLLKADKSHDRLDNDARLBZBACHSNAHHGNLDLLBGSTICFAAHDCXRPEETDCMQDWKBDPSBYJETBLGKTDNDRMSHHRFDIBEDREOLEKTBGMTBBOTKPFPWFWNAHTAADEHOEADAEAOADAMTAADDYOTADLNCSEHYKADYKAEYKAOCYTBFSSSOSAXAXAYCYINOTTORLASIEGRJPKPKSRYCEWNPF", + # testnet native segwit + "UR:CRYPTO-OUTPUT/TAADMWTAADDLOSAOWKAXHDCLAXRFBBVARYHNRLIMTLFEASUEOSGSRYOLBTCTSAHTIHKIDAMNPKLKFZTBHHMKKGKKGUAAHDCXHKMEMSKTVLNLSWDLUOGRDYBSFYIYIEJLCYVSJLNLMDLPJPNNGSSFMEIHHYCHLGENAHTAADEHOEADAEAOADAMTAADDYOTADLNCSGHYKADYKAEYKAOCYTBFSSSOSAXAXAYCYIDWEJPPKASIHFWGAGDEOESYKNYPEOT", + # testnet taproot + "UR:CRYPTO-OUTPUT/TAADNLTAADDLOSAOWKAXHDCLAOUYWTLNPRWYDIBNZCWEMHVWSTJYLGCNISEOPSEETEDMZTBSYTATJLAEZMNYDEEHCAAAHDCXHTRLUYDIRDNLDSYAZEKESWDYMTWNGSVAKTVWFGFEGUCSDNPDDTFRZMRTUTNSBSGTAHTAADEHOEADAEAOADAMTAADDYOTADLNCSHFYKADYKADYKAOCYTBFSSSOSAXAXAYCYOEWNPFHKASIMGUIHIHIEGUINIOJTIHJPDKZSVSIS", + ] + DESCRIPTORS = [ + "pkh([d63dc4a7/44h/1h/0h]tpubDCxxwY2QwiUCq8ievg9BurvmxfSa8LStd6XLh6meDBjK2BWynxq8M7d99P9yNBaCxSkUcxZrvnwgUsXbqP8SyJZY21C7Cm1R7M36xxSeiS6)", + "sh(wpkh([d63dc4a7/49h/1h/0h]tpubDCoS7zq26q1CC75oB7zcNRvbXbQwAUf9gnhaS5vDYEZA2F8Maz7taNiKGbEWGY1fxEyWQgvCCQRGNBz87qej9XuyMXpTCtDbbKP6fXGtio1))", + "wpkh([d63dc4a7/84h/1h/0h]tpubDCka9mfAaAN3cit16QqK2RCRVBpA2B7b8RFWUCLdyKR8eA48pjX7kZ5RAM6bSdwD9ivwh33KES7Q4DcqzNwkNUyyadZTTLf36Xp955vUBdf)", + "tr([d63dc4a7/86h/1h/1h]tpubDDDs2UVFbhXBRTW4EJEH74qDcdVetqn5pF2AMcpMCfwvU561GJiY4BZoLPSpw4d6cb7wVHyu1JNa3dVjhKgmTKZ6m8M4L6wv763gHjhYAfd)", + ] + + for i, QRDATUM in enumerate(QRDATA): + wallet_data = URDecoder().decode(QRDATUM) + descriptor, label = parse_wallet(wallet_data) + assert str(descriptor) == DESCRIPTORS[i] + print(DESCRIPTORS[i]) + + +def test_parse_wallet_via_ur_account(mocker, m5stickv): + from krux.wallet import parse_wallet + + # seedsigner uses this ur format when exporting for sparrow + QRDATA = [ + # mainnet legacy p2pkh + "UR:CRYPTO-ACCOUNT/OEADCYTBFSSSOSAOLYTAADMUTAADDLOXAXHDCLAODNLFAHCFGUJOYLESGDYNVANSDRGYTISWRTMWTTIAGUADAMPLONLAAEKPZOLGHSPMAAHDCXOLEOFRMSGELTHFPRJNVEECBBHDGOONEHMHBBTDVDAMCXVDMDROBZCWUYLDRERNHSAMTAADDYOTADLNCSDWYKAEYKAEYKAOCYTBFSSSOSAXAXAYCYBTWMBGPTAXUOCEVY", + # mainnet nested segwit + "UR:CRYPTO-ACCOUNT/OEADCYTBFSSSOSAOLYTAADMHTAADMWTAADDLOXAXHDCLAOFNVAJKFZAXCPCMJSAYYNYLCHEYESNLWDISVTVODAVTFZVEWLAMWYLDBNNTBEKECTAAHDCXSPYLEYCFMSHKJLGOMTTTEHCMNEPYSEIADTKPNBPMPKONIEWYAHPRADWDONYNGMSKAMTAADDYOTADLNCSEHYKAEYKAEYKAOCYTBFSSSOSAXAXAYCYVEZSGMCXTKYTCMKN", + # mainnet native segwit + "UR:CRYPTO-ACCOUNT/OEADCYTBFSSSOSAOLYTAADMWTAADDLOXAXHDCLAXZOCTGEIDHKEHSBHSDEZTJZWZJLFLMYAARSRFBWCLIDNELEVDDTWEDAMYURAECKSAAAHDCXGOSKCMDTKPFZJZSRJZHFHDQZJYLTIAZTQZMTZOCAIOCEECUESKAYECWFKPJZGOSPAMTAADDYOTADLNCSGHYKAEYKAEYKAOCYTBFSSSOSAXAXAYCYTBTEAXAODTCFGMBK", + # mainnet taproot + "UR:CRYPTO-ACCOUNT/OEADCYTBFSSSOSAOLYTAADNLTAADDLOXAXHDCLAXLKOENLDTDIFXOYDYUYHGOLFXSESOQZOXWSRTMHINPMSGKPNSHYIEBZVYVYRODKCEAAHDCXFTIDJNMSDWWPFDZSBNLSAORSKGGHTPGYFERSQDSFVSFWFYCYAOINCLWFLKSPMHFYAMTAADDYOTADLNCSHFYKAEYKAEYKAOCYTBFSSSOSAXAXAYCYKIWKJKVDYKTDJNUY", + ] + DESCRIPTORS = [ + "pkh([d63dc4a7/44h/0h/0h]xpub6Bkhh15pTDsqX2kEBaqLL9YTNwMBNkn8eL4vPEzPkKPG3dU1CtgNEfz6qMRH7ek8gonFWUe73Jg6Z2zVpYZMMgoXKsAnsuyFF6B3kZX81ed)", + "sh(wpkh([d63dc4a7/49h/0h/0h]xpub6DLPFknTv1YuRkTAmXXkrdcMVtsWjwMG1WkCKgQqP2LbEfhbn4EyYGo8gxgqF6jozRfQEVafJRnt57Ua2gXXBqyPvKPzuQRQpfYbyEUh8Z1))", + "wpkh([d63dc4a7/84h/0h/0h]xpub6DEMJ2Yce8xR9qAD4ZNfV4HuhhYCgNYbjDoZnuJprRQBtZCE7fgTLfLYdao26s3Pva1PBySwnSx9AtRtpSC3AsJ6LYuhKy1brMoc3Qe16t7)", + "tr([d63dc4a7/86h/0h/0h]xpub6CaTuhnAha3kjK1bZQxYbmyw8PcM6UypdK9MTdJY41Xxyg2dVws9LwuB5bL1fFPqJiAkBAoSwpcfEjEwwJ2byNeM4xxXKSa45TcHxLhQKPh)", + ] + + for i, QRDATUM in enumerate(QRDATA): + wallet_data = URDecoder().decode(QRDATUM) + descriptor, label = parse_wallet(wallet_data) + assert str(descriptor) == DESCRIPTORS[i] + print(DESCRIPTORS[i]) From 0edcf0108d5a60df6b6074bcebc32c1c593fd6a0 Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 24 Jul 2024 17:09:09 -0300 Subject: [PATCH 08/14] allow signing base64 encoded PSBTs from SD card --- src/krux/pages/home_pages/home.py | 17 +++++++---- src/krux/psbt.py | 51 ++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index 017eae2aa..86729f8fc 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -365,18 +365,18 @@ def sign_psbt(self): title = t("Signed PSBT") if index == 0: # Sign to QR code - qr_signed_psbt, qr_format = signer.psbt_qr() + signed_psbt, qr_format = signer.psbt_qr() # memory management del signer gc.collect() - self.display_qr_codes(qr_signed_psbt, qr_format) + self.display_qr_codes(signed_psbt, qr_format) from ..utils import Utils utils = Utils(self.ctx) - utils.print_standard_qr(qr_signed_psbt, qr_format, title, width=45) + utils.print_standard_qr(signed_psbt, qr_format, title, width=45) return MENU_CONTINUE # index == 1: Sign to SD card @@ -393,9 +393,14 @@ def sign_psbt(self): gc.collect() if psbt_filename and psbt_filename != ESC_KEY: - with open("/sd/" + psbt_filename, "wb") as f: - # Write PSBT data directly to the file - signer.psbt.write_to(f) + if signer.is_b64_file: + signed_psbt, _ = signer.psbt_qr() + with open("/sd/" + psbt_filename, "w") as f: + f.write(signed_psbt) + else: + with open("/sd/" + psbt_filename, "wb") as f: + # Write PSBT data directly to the file + signer.psbt.write_to(f) self.flash_text(t("Saved to SD card") + ":\n%s" % psbt_filename) return MENU_CONTINUE diff --git a/src/krux/psbt.py b/src/krux/psbt.py index 32aa6cced..48bb9a87a 100644 --- a/src/krux/psbt.py +++ b/src/krux/psbt.py @@ -55,23 +55,34 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): self.ur_type = None self.qr_format = qr_format self.policy = None + self.is_b64_file = False + # Parse the PSBT if psbt_filename: gc.collect() from .sd_card import SD_PATH + file_path = "/%s/%s" % (SD_PATH, psbt_filename) try: - file_path = "/%s/%s" % (SD_PATH, psbt_filename) - with open(file_path, "rb") as file: - self.psbt = PSBT.read_from(file, compress=1) - try: - self.validate() - except: - # Legacy will fail to get policy from compressed PSBT - # so we load it uncompressed - self.policy = None # Reset policy - file.seek(0) # Reset the file pointer to the beginning - self.psbt = PSBT.read_from(file) + self.is_b64_file = self.file_is_base64_encoded(file_path) + if self.is_b64_file: + # BlueWallet exports PSBTs as base64 encoded files + from .sd_card import SDHandler + + with SDHandler() as sd: + psbt_data = sd.read(psbt_filename) + self.psbt = PSBT.parse(base_decode(psbt_data, 64)) + else: + with open(file_path, "rb") as file: + self.psbt = PSBT.read_from(file, compress=1) + try: + self.validate() + except: + # Legacy will fail to get policy from compressed PSBT + # so we load it uncompressed + self.policy = None # Reset policy + file.seek(0) # Reset the file pointer to the beginning + self.psbt = PSBT.read_from(file) self.base_encoding = 64 # In case it is exported as QR code except Exception as e: raise ValueError("Error loading PSBT file: %s" % e) @@ -114,6 +125,24 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): except Exception as e: raise ValueError("Invalid PSBT: %s" % e) + def file_is_base64_encoded(self, file_path, chunk_size=64): + """Checks if a file is base64 encoded""" + try: + with open(file_path, "rb") as file: + chunk = file.read(chunk_size) + # Check if chunk length is divisible by 4 + if len(chunk) % 4 != 0: + return False + try: + # Try to decode the chunk as base64 + base_decode(chunk, 64) + return True + except Exception: + return False + except Exception as e: + print("Error opening file:", e) + return False + def validate(self): """Validates the PSBT""" # From: https://github.com/diybitcoinhardware/embit/blob/master/examples/change.py#L110 From 62c061b55db6f1b98075b1512a01eb49fe5d0384 Mon Sep 17 00:00:00 2001 From: odudex Date: Thu, 25 Jul 2024 14:04:34 -0300 Subject: [PATCH 09/14] sd card b64 signing - refactor and tests --- src/krux/psbt.py | 45 ++++++++++++++++---------------- tests/test_psbt.py | 64 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/src/krux/psbt.py b/src/krux/psbt.py index 48bb9a87a..b516edb08 100644 --- a/src/krux/psbt.py +++ b/src/krux/psbt.py @@ -64,28 +64,28 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): file_path = "/%s/%s" % (SD_PATH, psbt_filename) try: - self.is_b64_file = self.file_is_base64_encoded(file_path) - if self.is_b64_file: - # BlueWallet exports PSBTs as base64 encoded files - from .sd_card import SDHandler - - with SDHandler() as sd: - psbt_data = sd.read(psbt_filename) - self.psbt = PSBT.parse(base_decode(psbt_data, 64)) - else: - with open(file_path, "rb") as file: - self.psbt = PSBT.read_from(file, compress=1) - try: - self.validate() - except: - # Legacy will fail to get policy from compressed PSBT - # so we load it uncompressed - self.policy = None # Reset policy + with open(file_path, "rb") as file: + self.psbt = PSBT.read_from(file, compress=1) + self.validate() + except: + try: + self.policy = None # Reset policy + self.is_b64_file = self.file_is_base64_encoded(file_path) + if self.is_b64_file: + # BlueWallet exports PSBTs as base64 encoded files + # So it will be decoded and loaded uncompressed + with open(file_path, "r") as file: + psbt_data = file.read() + self.psbt = PSBT.parse(base_decode(psbt_data, 64)) + else: + # Legacy will fail to get policy from compressed PSBT + # so we load it uncompressed + with open(file_path, "rb") as file: file.seek(0) # Reset the file pointer to the beginning self.psbt = PSBT.read_from(file) - self.base_encoding = 64 # In case it is exported as QR code - except Exception as e: - raise ValueError("Error loading PSBT file: %s" % e) + except Exception as e: + raise ValueError("Error loading PSBT file: %s" % e) + self.base_encoding = 64 # In case it is exported as QR code elif isinstance(psbt_data, UR): try: self.psbt = PSBT.parse( @@ -131,7 +131,7 @@ def file_is_base64_encoded(self, file_path, chunk_size=64): with open(file_path, "rb") as file: chunk = file.read(chunk_size) # Check if chunk length is divisible by 4 - if len(chunk) % 4 != 0: + if not chunk or len(chunk) % 4 != 0: return False try: # Try to decode the chunk as base64 @@ -139,8 +139,7 @@ def file_is_base64_encoded(self, file_path, chunk_size=64): return True except Exception: return False - except Exception as e: - print("Error opening file:", e) + except Exception: return False def validate(self): diff --git a/tests/test_psbt.py b/tests/test_psbt.py index d4484cb6f..8a1f1c67c 100644 --- a/tests/test_psbt.py +++ b/tests/test_psbt.py @@ -237,13 +237,19 @@ class MockFile: """Custom mock file class that supports read, write, seek, and other file methods""" def __init__(self, data=b""): - self.data = data + self.data = data if isinstance(data, bytes) else data.encode() + self.position = 0 + self.write_data = bytearray() + self.mode = "rb" self.file = MagicMock() self.file.read.side_effect = self.read self.file.write.side_effect = self.write self.file.seek.side_effect = self.seek - self.position = 0 - self.write_data = b"" + + def set_mode(self, mode): + self.mode = mode + if "b" not in mode: + self.write_data = "" def seek(self, pos): self.position = pos @@ -253,19 +259,40 @@ def read(self, size=None): size = len(self.data) - self.position result = self.data[self.position : self.position + size] self.position += size + if "b" not in self.mode: + return result.decode() return result def write(self, content): - self.write_data += content + print("mode", self.mode) + if "b" not in self.mode: + if isinstance(content, str): + self.write_data += content + else: + raise TypeError("write() argument must be str") + else: + if isinstance(content, bytearray) or isinstance(content, bytes): + self.write_data.extend(content) + else: + raise TypeError("A bytes-like object is required, not 'str'") return len(content) def __enter__(self): - return self + self.position = 0 + return self.file def __exit__(self, *args): pass +def mock_open(mock_file): + def _open(file, mode="r", *args, **kwargs): + mock_file.set_mode(mode) + return mock_file + + return _open + + def test_init_singlesig(mocker, m5stickv, tdata): from embit.networks import NETWORKS from krux.psbt import PSBTSigner @@ -319,8 +346,7 @@ def test_init_singlesig_from_sdcard(mocker, m5stickv, tdata): ] for case in cases: - mock_file = MockFile(case[0]) - mocker.patch("builtins.open", return_value=mock_file) + mocker.patch("builtins.open", mock_open(MockFile(case[0]))) signer = PSBTSigner(wallet, None, case[1], "dummy.psbt") assert isinstance(signer, PSBTSigner) @@ -447,27 +473,39 @@ def test_sign_singlesig_from_sdcard(mocker, m5stickv, tdata): from krux.psbt import PSBTSigner from krux.key import Key from krux.wallet import Wallet - from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR + from krux.qr import FORMAT_NONE wallet = Wallet(Key(tdata.TEST_MNEMONIC, False, NETWORKS["test"])) cases = [ (tdata.P2PKH_PSBT, FORMAT_NONE, tdata.SIGNED_P2PKH_PSBT), + (tdata.P2PKH_PSBT_B64, FORMAT_NONE, tdata.SIGNED_P2PKH_PSBT_B64), (tdata.P2WPKH_PSBT, FORMAT_NONE, tdata.SIGNED_P2WPKH_PSBT), + (tdata.P2WPKH_PSBT_B64, FORMAT_NONE, tdata.SIGNED_P2WPKH_PSBT_B64), (tdata.P2SH_P2WPKH_PSBT, FORMAT_NONE, tdata.SIGNED_P2SH_P2WPKH_PSBT), + (tdata.P2SH_P2WPKH_PSBT_B64, FORMAT_NONE, tdata.SIGNED_P2SH_P2WPKH_PSBT_B64), (tdata.P2TR_PSBT, FORMAT_NONE, tdata.SIGNED_P2TR_PSBT), + (tdata.P2TR_PSBT_B64, FORMAT_NONE, tdata.SIGNED_P2TR_PSBT_B64), ] num = 0 for case in cases: - print("test_sign_singlesig case: ", num) - num += 1 + print("test_sign_singlesig_from_sdcard case: ", num) mock_file = MockFile(case[0]) - mocker.patch("builtins.open", return_value=mock_file) + mocker.patch("builtins.open", mock_open(mock_file)) signer = PSBTSigner(wallet, None, case[1], "dummy.psbt") signer.sign() - with open("dummy-signed.psbt", "wb") as f: - signer.psbt.write_to(f) + if num % 2 == 1: + # If test case num is odd, check if detected as base64 + assert signer.is_b64_file + signed_psbt, _ = signer.psbt_qr() + print("signed_psbt", signed_psbt) + with open("/sd/" + "dummy-signed.psbt", "w") as f: + f.write(signed_psbt) + else: + with open("/sd/" + "dummy-signed.psbt", "wb") as f: + signer.psbt.write_to(f) assert mock_file.write_data == case[2] + num += 1 def test_sign_multisig(mocker, m5stickv, tdata): From 3b82bb1d110e1e18e972c69a19ce500b9cf7cde9 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Fri, 26 Jul 2024 17:31:03 -0300 Subject: [PATCH 10/14] About shows board hardware --- src/krux/pages/login.py | 7 ++++++- tests/pages/test_login.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 8260a5c26..eefa910b6 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -723,11 +723,16 @@ def settings(self): def about(self): """Handler for the 'about' menu item""" + import board from ..metadata import VERSION self.ctx.display.clear() self.ctx.display.draw_centered_text( - "Krux\n\n\n" + t("Version") + "\n%s" % VERSION + "Krux\n\n" + + t("Hardware") + + "\n%s\n\n" % board.config["type"] + + t("Version") + + "\n%s" % VERSION ) self.ctx.input.wait_for_button() return MENU_CONTINUE diff --git a/tests/pages/test_login.py b/tests/pages/test_login.py index 6b7df45c3..34b1d64ed 100644 --- a/tests/pages/test_login.py +++ b/tests/pages/test_login.py @@ -1185,6 +1185,7 @@ def test_customization_while_loading_wallet(amigo, mocker): def test_about(mocker, m5stickv): import krux from krux.pages.login import Login + import board from krux.metadata import VERSION from krux.input import BUTTON_ENTER @@ -1197,4 +1198,6 @@ def test_about(mocker, m5stickv): login.about() ctx.input.wait_for_button.assert_called_once() - ctx.display.draw_centered_text.assert_called_with("Krux\n\n\nVersion\n" + VERSION) + ctx.display.draw_centered_text.assert_called_with( + "Krux\n\nHardware\n" + board.config["type"] + "\n\nVersion\n" + VERSION + ) From 2fb5b9b6732b5287c9931954b47c395502305426 Mon Sep 17 00:00:00 2001 From: odudex Date: Tue, 30 Jul 2024 11:06:52 -0300 Subject: [PATCH 11/14] remove debug print --- tests/test_psbt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_psbt.py b/tests/test_psbt.py index 8a1f1c67c..4b1ae1552 100644 --- a/tests/test_psbt.py +++ b/tests/test_psbt.py @@ -264,7 +264,6 @@ def read(self, size=None): return result def write(self, content): - print("mode", self.mode) if "b" not in self.mode: if isinstance(content, str): self.write_data += content From fd42157c22dad8c28919577f93dedd7ffe722f16 Mon Sep 17 00:00:00 2001 From: odudex Date: Tue, 30 Jul 2024 13:18:37 -0300 Subject: [PATCH 12/14] b64 PSBT from SD card: refactor and more tests --- src/krux/psbt.py | 27 ++++----- tests/pages/home_pages/test_home.py | 51 +++++++++------- tests/shared_mocks.py | 60 +++++++++++++++++++ tests/test_psbt.py | 92 ++++++++++------------------- 4 files changed, 134 insertions(+), 96 deletions(-) diff --git a/src/krux/psbt.py b/src/krux/psbt.py index b516edb08..485023fb5 100644 --- a/src/krux/psbt.py +++ b/src/krux/psbt.py @@ -127,20 +127,19 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): def file_is_base64_encoded(self, file_path, chunk_size=64): """Checks if a file is base64 encoded""" - try: - with open(file_path, "rb") as file: - chunk = file.read(chunk_size) - # Check if chunk length is divisible by 4 - if not chunk or len(chunk) % 4 != 0: - return False - try: - # Try to decode the chunk as base64 - base_decode(chunk, 64) - return True - except Exception: - return False - except Exception: - return False + with open(file_path, "rb") as file: + chunk = file.read(chunk_size) + if not chunk: + raise ValueError("Empty file") + # Check if chunk length is divisible by 4 + if len(chunk) % 4 != 0: + return False + try: + # Try to decode the chunk as base64 + base_decode(chunk, 64) + return True + except Exception: + return False def validate(self): """Validates the PSBT""" diff --git a/tests/pages/home_pages/test_home.py b/tests/pages/home_pages/test_home.py index 5d4e4b746..d03bb78a0 100644 --- a/tests/pages/home_pages/test_home.py +++ b/tests/pages/home_pages/test_home.py @@ -330,6 +330,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.qr import FORMAT_PMOFN, FORMAT_NONE from krux.sd_card import PSBT_FILE_EXTENSION, SIGNED_FILE_SUFFIX + from ...shared_mocks import MockFile, mock_open cases = [ # Single-sig, not loaded, no format => pmofn, sign, No print prompt @@ -544,10 +545,10 @@ def test_sign_psbt(mocker, m5stickv, tdata): # Case 10 tdata.SINGLESIG_SIGNING_KEY, # 0 wallet None, - tdata.P2WPKH_PSBT, # 2 capture_qr_code return 1 - FORMAT_NONE, # 3 capture_qr_code return 2 + tdata.P2WPKH_PSBT, + FORMAT_NONE, True, - tdata.SIGNED_P2WPKH_PSBT_B64, + None, None, # 6 printer [ BUTTON_PAGE, # Move to "Load from SD card" @@ -569,7 +570,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): tdata.P2WSH_PSBT, FORMAT_NONE, True, - tdata.SIGNED_P2WSH_PSBT_B64, + None, None, [ BUTTON_ENTER, # Wallet not loaded, proceed? @@ -585,6 +586,28 @@ def test_sign_psbt(mocker, m5stickv, tdata): ], tdata.SIGNED_P2WSH_PSBT, # 8 SD avaiable ), + # Single-sig base64, not loaded, load from microSD, sign to microSD + ( + # Case 12 + tdata.SINGLESIG_SIGNING_KEY, + None, + tdata.P2WPKH_PSBT_B64, + FORMAT_NONE, + True, + None, + None, + [ + BUTTON_PAGE, # Move to "Load from SD card" + BUTTON_ENTER, # Load from SD card + BUTTON_ENTER, # Path mismatch ACK + BUTTON_ENTER, # PSBT resume + BUTTON_ENTER, # output 1 + BUTTON_ENTER, # output 2 + BUTTON_PAGE, # Move to "Sign to QR SD card" + BUTTON_ENTER, # Sign to SD card + ], + tdata.SIGNED_P2WPKH_PSBT_B64, # 8 SD avaiable + ), ] # Case X # [0] Wallet @@ -628,16 +651,8 @@ def test_sign_psbt(mocker, m5stickv, tdata): mocker.patch.object(home, "has_sd_card", new=lambda: True) mock_utils = mocker.patch("krux.pages.utils.Utils") mock_utils.return_value.load_file.return_value = (PSBT_FILE_NAME, None) - # Mock for reading from input file - mock_open_read = mocker.mock_open(read_data=case[2]) - # Mock for writing to output file - mock_open_write = mocker.mock_open() - # Ensure the write method returns the number of bytes written - mock_open_write.return_value.write.side_effect = lambda x: len(x) - mocker.patch( - "builtins.open", - side_effect=[mock_open_read.return_value, mock_open_write.return_value], - ) + mock_file = MockFile(case[2]) + mocker.patch("builtins.open", mock_open(mock_file)) mock_set_filename = mocker.patch( "krux.pages.file_operations.SaveFile.set_filename", return_value=SIGNED_PSBT_FILE_NAME, @@ -667,13 +682,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): SIGNED_FILE_SUFFIX, PSBT_FILE_EXTENSION, ) - # Get the mock file handle for writing - handle_write = mock_open_write() - # # Embit will write the signed PSBT to the output file in chunks. Capture all write calls - written_data = b"".join( - call.args[0] for call in handle_write.write.call_args_list - ) - assert written_data == case[8] + assert mock_file.write_data == case[8] home.display_qr_codes.assert_not_called() if case[6] is not None: # if has printer diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py index ab4c61561..5b3a0143e 100644 --- a/tests/shared_mocks.py +++ b/tests/shared_mocks.py @@ -1,9 +1,69 @@ import pytest from unittest import mock +from unittest.mock import MagicMock import pyqrcode import zlib +class MockFile: + """Custom mock file class that supports read, write, seek, and other file methods""" + + def __init__(self, data=b""): + self.data = data if isinstance(data, bytes) else data.encode() + self.position = 0 + self.write_data = bytearray() + self.mode = "rb" + self.file = MagicMock() + self.file.read.side_effect = self.read + self.file.write.side_effect = self.write + self.file.seek.side_effect = self.seek + + def set_mode(self, mode): + self.mode = mode + if "b" not in mode: + self.write_data = "" + + def seek(self, pos): + self.position = pos + + def read(self, size=None): + if size is None: + size = len(self.data) - self.position + result = self.data[self.position : self.position + size] + self.position += size + if "b" not in self.mode: + return result.decode() + return result + + def write(self, content): + if "b" not in self.mode: + if isinstance(content, str): + self.write_data += content + else: + raise TypeError("write() argument must be str") + else: + if isinstance(content, bytearray) or isinstance(content, bytes): + self.write_data.extend(content) + else: + raise TypeError("A bytes-like object is required, not 'str'") + return len(content) + + def __enter__(self): + self.position = 0 + return self.file + + def __exit__(self, *args): + pass + + +def mock_open(mock_file): + def _open(file, mode="r", *args, **kwargs): + mock_file.set_mode(mode) + return mock_file + + return _open + + class DeflateIO: def __init__(self, stream) -> None: self.stream = stream diff --git a/tests/test_psbt.py b/tests/test_psbt.py index 4b1ae1552..ecddde0a5 100644 --- a/tests/test_psbt.py +++ b/tests/test_psbt.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import MagicMock +from .shared_mocks import MockFile, mock_open @pytest.fixture @@ -233,65 +233,6 @@ def tdata(mocker): ) -class MockFile: - """Custom mock file class that supports read, write, seek, and other file methods""" - - def __init__(self, data=b""): - self.data = data if isinstance(data, bytes) else data.encode() - self.position = 0 - self.write_data = bytearray() - self.mode = "rb" - self.file = MagicMock() - self.file.read.side_effect = self.read - self.file.write.side_effect = self.write - self.file.seek.side_effect = self.seek - - def set_mode(self, mode): - self.mode = mode - if "b" not in mode: - self.write_data = "" - - def seek(self, pos): - self.position = pos - - def read(self, size=None): - if size is None: - size = len(self.data) - self.position - result = self.data[self.position : self.position + size] - self.position += size - if "b" not in self.mode: - return result.decode() - return result - - def write(self, content): - if "b" not in self.mode: - if isinstance(content, str): - self.write_data += content - else: - raise TypeError("write() argument must be str") - else: - if isinstance(content, bytearray) or isinstance(content, bytes): - self.write_data.extend(content) - else: - raise TypeError("A bytes-like object is required, not 'str'") - return len(content) - - def __enter__(self): - self.position = 0 - return self.file - - def __exit__(self, *args): - pass - - -def mock_open(mock_file): - def _open(file, mode="r", *args, **kwargs): - mock_file.set_mode(mode) - return mock_file - - return _open - - def test_init_singlesig(mocker, m5stickv, tdata): from embit.networks import NETWORKS from krux.psbt import PSBTSigner @@ -334,7 +275,7 @@ def test_init_singlesig_from_sdcard(mocker, m5stickv, tdata): from krux.psbt import PSBTSigner from krux.key import Key from krux.wallet import Wallet - from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR + from krux.qr import FORMAT_NONE wallet = Wallet(Key(tdata.TEST_MNEMONIC, False, NETWORKS["test"])) cases = [ @@ -350,6 +291,19 @@ def test_init_singlesig_from_sdcard(mocker, m5stickv, tdata): assert isinstance(signer, PSBTSigner) +def test_init_empty_file_from_sdcard(mocker, m5stickv, tdata): + from embit.networks import NETWORKS + from krux.psbt import PSBTSigner + from krux.key import Key + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(Key(tdata.TEST_MNEMONIC, False, NETWORKS["test"])) + mocker.patch("builtins.open", mock_open(MockFile())) + with pytest.raises(ValueError): + PSBTSigner(wallet, None, FORMAT_NONE, "dummy.psbt") + + def test_init_multisig(mocker, m5stickv, tdata): from embit.networks import NETWORKS from krux.psbt import PSBTSigner @@ -415,6 +369,22 @@ def test_init_fails_on_invalid_psbt(mocker, m5stickv, tdata): PSBTSigner(wallet, case[0], case[1]) +def test_init_fails_on_invalid_psbt_from_sdcard(mocker, m5stickv, tdata): + from embit.networks import NETWORKS + from ur.ur import UR + from krux.psbt import PSBTSigner + from krux.key import Key + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE, FORMAT_UR + + wallet = Wallet(Key(tdata.TEST_MNEMONIC, False, NETWORKS["test"])) + + mock_file = MockFile("thisisnotavalidpsbt") + mocker.patch("builtins.open", return_value=mock_file) + with pytest.raises(ValueError): + PSBTSigner(wallet, None, FORMAT_NONE, "dummy.psbt") + + def test_sign_singlesig(mocker, m5stickv, tdata): from embit.networks import NETWORKS from krux.psbt import PSBTSigner From 88c259081c559e886a372e94e9f7dca247aadd2b Mon Sep 17 00:00:00 2001 From: odudex Date: Tue, 30 Jul 2024 15:31:07 -0300 Subject: [PATCH 13/14] B64 PSBT from SD card: deal with .psbt.txt extension --- src/krux/pages/home_pages/home.py | 44 ++++++++++++++++++++--------- src/krux/sd_card.py | 1 + tests/pages/home_pages/test_home.py | 30 ++++++++++++++++---- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index 86729f8fc..46e8b8656 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -209,11 +209,13 @@ def load_psbt(self): # If load_method == LOAD_FROM_SD from ..utils import Utils - from ...sd_card import PSBT_FILE_EXTENSION + from ...sd_card import PSBT_FILE_EXTENSION, B64_PSBT_FILE_EXTENSION utils = Utils(self.ctx) psbt_filename, _ = utils.load_file( - PSBT_FILE_EXTENSION, prompt=False, only_get_filename=True + [PSBT_FILE_EXTENSION, B64_PSBT_FILE_EXTENSION], + prompt=False, + only_get_filename=True, ) return (None, FORMAT_NONE, psbt_filename) @@ -232,12 +234,35 @@ def _sign_menu(self): index, _ = sign_menu.run_loop() return index - def sign_psbt(self): - """Handler for the 'sign psbt' menu item""" + def _format_psbt_file_extension(self, psbt_filename=""): + """Formats the PSBT filename""" from ...sd_card import ( PSBT_FILE_EXTENSION, + B64_PSBT_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) + from ..file_operations import SaveFile + + if psbt_filename.endswith(B64_PSBT_FILE_EXTENSION): + # Remove chained extensions + psbt_filename = psbt_filename[: -len(B64_PSBT_FILE_EXTENSION)] + if psbt_filename.endswith(PSBT_FILE_EXTENSION): + psbt_filename = psbt_filename[: -len(PSBT_FILE_EXTENSION)] + extension = PSBT_FILE_EXTENSION + B64_PSBT_FILE_EXTENSION + else: + extension = PSBT_FILE_EXTENSION + + save_page = SaveFile(self.ctx) + psbt_filename = save_page.set_filename( + psbt_filename, + "QRCode", SIGNED_FILE_SUFFIX, + extension, ) + return psbt_filename + + def sign_psbt(self): + """Handler for the 'sign psbt' menu item""" # Warns in case multisig wallet descriptor is not loaded if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig(): @@ -380,16 +405,7 @@ def sign_psbt(self): return MENU_CONTINUE # index == 1: Sign to SD card - from ..file_operations import SaveFile - - save_page = SaveFile(self.ctx) - psbt_filename = save_page.set_filename( - psbt_filename, - "QRCode", - SIGNED_FILE_SUFFIX, - PSBT_FILE_EXTENSION, - ) - del save_page + psbt_filename = self._format_psbt_file_extension(psbt_filename) gc.collect() if psbt_filename and psbt_filename != ESC_KEY: diff --git a/src/krux/sd_card.py b/src/krux/sd_card.py index 642579158..8b52dbdd5 100644 --- a/src/krux/sd_card.py +++ b/src/krux/sd_card.py @@ -25,6 +25,7 @@ SIGNED_FILE_SUFFIX = "-signed" PSBT_FILE_EXTENSION = ".psbt" +B64_PSBT_FILE_EXTENSION = ".txt" DESCRIPTOR_FILE_EXTENSION = ".txt" JSON_FILE_EXTENSION = ".json" SIGNATURE_FILE_EXTENSION = ".sig" diff --git a/tests/pages/home_pages/test_home.py b/tests/pages/home_pages/test_home.py index d03bb78a0..667976559 100644 --- a/tests/pages/home_pages/test_home.py +++ b/tests/pages/home_pages/test_home.py @@ -329,7 +329,11 @@ def test_sign_psbt(mocker, m5stickv, tdata): from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.qr import FORMAT_PMOFN, FORMAT_NONE - from krux.sd_card import PSBT_FILE_EXTENSION, SIGNED_FILE_SUFFIX + from krux.sd_card import ( + PSBT_FILE_EXTENSION, + B64_PSBT_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) from ...shared_mocks import MockFile, mock_open cases = [ @@ -674,7 +678,9 @@ def test_sign_psbt(mocker, m5stickv, tdata): if case[8] is not None: # if signed from/to SD card mock_utils.return_value.load_file.assert_called_once_with( - ".psbt", prompt=False, only_get_filename=True + [PSBT_FILE_EXTENSION, B64_PSBT_FILE_EXTENSION], + prompt=False, + only_get_filename=True, ) mock_set_filename.assert_called_once_with( PSBT_FILE_NAME, @@ -699,7 +705,11 @@ def test_psbt_warnings(mocker, m5stickv, tdata): from krux.pages.home_pages.home import Home from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE - from krux.sd_card import PSBT_FILE_EXTENSION, SIGNED_FILE_SUFFIX + from krux.sd_card import ( + PSBT_FILE_EXTENSION, + B64_PSBT_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) PSBT_FILE_NAME = "test.psbt" SIGNED_PSBT_FILE_NAME = "test-signed.psbt" @@ -774,7 +784,9 @@ def test_psbt_warnings(mocker, m5stickv, tdata): # signed from/to SD card mock_utils.return_value.load_file.assert_called_once_with( - ".psbt", prompt=False, only_get_filename=True + [PSBT_FILE_EXTENSION, B64_PSBT_FILE_EXTENSION], + prompt=False, + only_get_filename=True, ) mock_set_filename.assert_called_once_with( PSBT_FILE_NAME, @@ -871,7 +883,11 @@ def test_sign_p2tr_zeroes_fingerprint(mocker, m5stickv, tdata): from krux.pages.home_pages.home import Home from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE - from krux.sd_card import PSBT_FILE_EXTENSION, SIGNED_FILE_SUFFIX + from krux.sd_card import ( + PSBT_FILE_EXTENSION, + B64_PSBT_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) PSBT_FILE_NAME = "test.psbt" SIGNED_PSBT_FILE_NAME = "test-signed.psbt" @@ -919,7 +935,9 @@ def test_sign_p2tr_zeroes_fingerprint(mocker, m5stickv, tdata): # signed from/to SD card mock_utils.return_value.load_file.assert_called_once_with( - ".psbt", prompt=False, only_get_filename=True + [PSBT_FILE_EXTENSION, B64_PSBT_FILE_EXTENSION], + prompt=False, + only_get_filename=True, ) mock_set_filename.assert_called_once_with( PSBT_FILE_NAME, From fb929b4585f3803cc77fa48ad7c635a11c74c877 Mon Sep 17 00:00:00 2001 From: odudex Date: Tue, 30 Jul 2024 15:54:34 -0300 Subject: [PATCH 14/14] B64 PSBT from SD card: add tests to .psbt.txt extension --- tests/pages/home_pages/test_home.py | 56 +++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/tests/pages/home_pages/test_home.py b/tests/pages/home_pages/test_home.py index 667976559..9ca27b616 100644 --- a/tests/pages/home_pages/test_home.py +++ b/tests/pages/home_pages/test_home.py @@ -624,13 +624,22 @@ def test_sign_psbt(mocker, m5stickv, tdata): # [7] Button Sequence # [8] Signed PSBT Data exported to SD card - PSBT_FILE_NAME = "test.psbt" - SIGNED_PSBT_FILE_NAME = "test-signed.psbt" + PSBT_FILE_NAME_NO_EXT = "test" + PSBT_FILE_NAME = PSBT_FILE_NAME_NO_EXT + PSBT_FILE_EXTENSION + B64_PSBT_FILE_NAME = ( + PSBT_FILE_NAME_NO_EXT + PSBT_FILE_EXTENSION + B64_PSBT_FILE_EXTENSION + ) + SIGNED_PSBT_FILE_NAME = PSBT_FILE_NAME_NO_EXT + "-signed" + PSBT_FILE_EXTENSION + B64_SIGNED_PSBT_FILE_NAME = ( + PSBT_FILE_NAME_NO_EXT + + "-signed" + + PSBT_FILE_EXTENSION + + B64_PSBT_FILE_EXTENSION + ) num = 0 for case in cases: print("test_sign_psbt", num) - num += 1 wallet = Wallet(case[0]) if case[1] is not None: wallet.load(case[1], FORMAT_PMOFN) @@ -654,13 +663,23 @@ def test_sign_psbt(mocker, m5stickv, tdata): if case[8] is not None: mocker.patch.object(home, "has_sd_card", new=lambda: True) mock_utils = mocker.patch("krux.pages.utils.Utils") - mock_utils.return_value.load_file.return_value = (PSBT_FILE_NAME, None) mock_file = MockFile(case[2]) mocker.patch("builtins.open", mock_open(mock_file)) - mock_set_filename = mocker.patch( - "krux.pages.file_operations.SaveFile.set_filename", - return_value=SIGNED_PSBT_FILE_NAME, - ) + if num == 12: # test a B64 .psbt.txt file extension + mock_utils.return_value.load_file.return_value = ( + B64_PSBT_FILE_NAME, + None, + ) + mock_set_filename = mocker.patch( + "krux.pages.file_operations.SaveFile.set_filename", + return_value=B64_SIGNED_PSBT_FILE_NAME, + ) + else: + mock_utils.return_value.load_file.return_value = (PSBT_FILE_NAME, None) + mock_set_filename = mocker.patch( + "krux.pages.file_operations.SaveFile.set_filename", + return_value=SIGNED_PSBT_FILE_NAME, + ) home.sign_psbt() assert ctx.input.wait_for_button.call_count == len(case[7]) @@ -682,12 +701,20 @@ def test_sign_psbt(mocker, m5stickv, tdata): prompt=False, only_get_filename=True, ) - mock_set_filename.assert_called_once_with( - PSBT_FILE_NAME, - "QRCode", - SIGNED_FILE_SUFFIX, - PSBT_FILE_EXTENSION, - ) + if num == 12: # test a B64 .psbt.txt file extension + mock_set_filename.assert_called_once_with( + PSBT_FILE_NAME_NO_EXT, + "QRCode", + SIGNED_FILE_SUFFIX, + PSBT_FILE_EXTENSION + B64_PSBT_FILE_EXTENSION, + ) + else: + mock_set_filename.assert_called_once_with( + PSBT_FILE_NAME, + "QRCode", + SIGNED_FILE_SUFFIX, + PSBT_FILE_EXTENSION, + ) assert mock_file.write_data == case[8] home.display_qr_codes.assert_not_called() @@ -696,6 +723,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): mock_send_to_printer.assert_called() else: # if declined to print mock_send_to_printer.assert_not_called() + num += 1 # TODO: Create cross test cases: Load from QR code, sign, save to SD card and vice versa # TODO: Import wallet descriptor and test signing