From fcd424201246ec3bc8953a86aa5fa89ed3353dac Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 13 Jul 2024 13:34:01 -0500 Subject: [PATCH 1/8] Light refactor of Electrum seed support --- src/seedsigner/models/encode_qr.py | 14 ++--- src/seedsigner/models/settings_definition.py | 5 +- src/seedsigner/views/psbt_views.py | 7 --- src/seedsigner/views/seed_views.py | 56 +++++++++++--------- src/seedsigner/views/tools_views.py | 12 +---- tests/screenshot_generator/generator.py | 2 + tests/test_encodepsbtqr.py | 9 ++-- tests/test_flows_seed.py | 51 ++++++++++++++++++ tests/test_seedqr.py | 4 +- 9 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 269a8f89..2bc33806 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -85,15 +85,17 @@ class BaseStaticQrEncoder(BaseQrEncoder): @dataclass class SeedQrEncoder(BaseStaticQrEncoder): - seed : Seed = None + mnemonic: List[str] = None + wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + def __post_init__(self): - self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code) + self.wordlist = Seed.get_wordlist(self.wordlist_language_code) super().__post_init__() self.data = "" # Output as Numeric data format - for word in self.seed.mnemonic_list: + for word in self.mnemonic: index = self.wordlist.index(word) self.data += str("%04d" % index) @@ -108,18 +110,18 @@ class CompactSeedQrEncoder(SeedQrEncoder): def next_part(self): # Output as binary data format binary_str = "" - for word in self.seed.mnemonic_list: + for word in self.mnemonic: index = self.wordlist.index(word) # Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits binary_str += bin(index).split('b')[1].zfill(11) # We can exclude the checksum bits at the end - if len(self.seed.mnemonic_list) == 24: + if len(self.mnemonic) == 24: # 8 checksum bits in a 24-word seed binary_str = binary_str[:-8] - elif len(self.seed.mnemonic_list) == 12: + elif len(self.mnemonic) == 12: # 4 checksum bits in a 12-word seed binary_str = binary_str[:-4] diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 8f33ee6b..c5334018 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -470,8 +470,9 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, - abbreviated_name="Electrum", - display_name="Electrum seed support (Native Segwit only)", + abbreviated_name="electrum", + display_name="Electrum seeds", + help_text="Native Segwit only", visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index b4019e33..16822882 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -79,13 +79,6 @@ class PSBTSelectSeedView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - self.run_screen( - WarningScreen, - title="Electrum warning", - status_headline=None, - text=f"Some features disabled for Electrum seeds", - show_back_button=False, - ) from seedsigner.views.seed_views import SeedMnemonicEntryView self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) return Destination(SeedMnemonicEntryView) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index c4fbc4ed..069737ec 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -119,6 +119,7 @@ class SeedSelectSeedView(View): button_data.append(self.SCAN_SEED) button_data.append(self.TYPE_12WORD) button_data.append(self.TYPE_24WORD) + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: button_data.append(self.TYPE_ELECTRUM) @@ -158,17 +159,7 @@ class SeedSelectSeedView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - self.run_screen( - WarningScreen, - title="Electrum warning", - status_headline=None, - text=f"Some features disabled for Electrum seeds", - show_back_button=False, - ) - - from seedsigner.views.seed_views import SeedMnemonicEntryView - self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) - return Destination(SeedMnemonicEntryView) + return Destination(SeedElectrumMnemonicStartView) @@ -187,10 +178,12 @@ class LoadSeedView(View): self.SEED_QR, self.TYPE_12WORD, self.TYPE_24WORD, - self.CREATE, ] + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: - button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM) + button_data.append(self.TYPE_ELECTRUM) + + button_data.append(self.CREATE) selected_menu_num = self.run_screen( ButtonListScreen, @@ -215,15 +208,7 @@ class LoadSeedView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - self.run_screen( - WarningScreen, - title="Electrum warning", - status_headline=None, - text=f"Some features disabled for Electrum seeds", - show_back_button=False, - ) - self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) - return Destination(SeedMnemonicEntryView) + return Destination(SeedElectrumMnemonicStartView) elif button_data[selected_menu_num] == self.CREATE: from .tools_views import ToolsMenuView @@ -489,6 +474,27 @@ class SeedDiscardView(View): +class SeedElectrumMnemonicStartView(View): + """ + Currently just a warning display before entering an Electrum seed. + + Could be expanded with a follow-up View to specify Electrum seed type. + """ + def run(self): + self.run_screen( + WarningScreen, + title="Electrum warning", + status_headline=None, + text=f"Some features are disabled for Electrum seeds.", + show_back_button=False, + ) + + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + + return Destination(SeedMnemonicEntryView) + + + """**************************************************************************** Views for actions on individual seeds: ****************************************************************************""" @@ -1456,7 +1462,8 @@ class SeedTranscribeSeedQRWholeQRView(View): def run(self): - encoder_args = dict(seed=self.seed) + encoder_args = dict(mnemonic=self.seed.mnemonic_list, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) if self.seedqr_format == QRType.SEED__SEEDQR: e = SeedQrEncoder(**encoder_args) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR: @@ -1492,7 +1499,8 @@ class SeedTranscribeSeedQRZoomedInView(View): def run(self): - encoder_args = dict(seed=self.seed) + encoder_args = dict(mnemonic=self.seed.mnemonic_list, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) if self.seedqr_format == QRType.SEED__SEEDQR: e = SeedQrEncoder(**encoder_args) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR: diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index ad2940be..fa908891 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -496,16 +496,8 @@ class ToolsAddressExplorerSelectSourceView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - self.run_screen( - WarningScreen, - title="Electrum warning", - status_headline=None, - text=f"Some features disabled for Electrum seeds", - show_back_button=False, - ) - from seedsigner.views.seed_views import SeedMnemonicEntryView - self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) - return Destination(SeedMnemonicEntryView) + from seedsigner.views.seed_views import SeedElectrumWarningView + return Destination(SeedElectrumWarningView) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 810e5d6f..e98c6541 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -215,6 +215,8 @@ def test_generate_screenshots(target_locale): seed_views.SeedSignMessageConfirmMessageView, seed_views.SeedSignMessageConfirmAddressView, + + seed_views.SeedElectrumMnemonicStartView, ], "PSBT Views": [ psbt_views.PSBTSelectSeedView, # this will fail, be rerun below diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 34f28631..14feb1e0 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -25,19 +25,18 @@ def test_ur_psbt_qr_encode(): def test_seedsigner_qr(): # test vector 1 from the SeedQR docs mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() - e = SeedQrEncoder(seed=Seed(mnemonic)) + e = SeedQrEncoder(mnemonic=mnemonic) assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643" - e = CompactSeedQrEncoder(seed=Seed(mnemonic)) + e = CompactSeedQrEncoder(mnemonic=mnemonic) assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n' # test vector 4 from the SeedQR docs mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split() - seed = Seed(mnemonic) - e = SeedQrEncoder(seed=seed) + e = SeedQrEncoder(mnemonic=mnemonic) assert e.next_part() == "073318950739065415961602009907670428187212261116" - e = CompactSeedQrEncoder(seed=seed) + e = CompactSeedQrEncoder(mnemonic=mnemonic) assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE' diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 63230319..e4fa201a 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -119,6 +119,57 @@ class TestSeedFlows(FlowTest): self.run_sequence(sequence) + def test_electrum_mnemonic_entry_flow(self): + """ + Manually entering an Electrum mnemonic should land at the Finalize Seed flow and end at + the SeedOptionsView. + + Most BIP-39 mnemonics should generate an error if entered as Electrum seeds. + """ + def test_with_mnemonic(mnemonic: list[str], expects_electrum_seed_is_valid: bool = True): + Settings.HOSTNAME = "not seedsigner-os" + settings = Settings.get_instance() + settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED) + + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView + FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_ELECTRUM), + FlowStep(seed_views.SeedElectrumMnemonicStartView), # Warning screen; no relevant button data selection. + ] + + # Now add each manual word entry step + for word in mnemonic: + sequence.append( + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word) + ) + + if expects_electrum_seed_is_valid: + # With the mnemonic completely entered, we land on the SeedFinalizeView + sequence += [ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView), + ] + + else: + sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD)) + + self.run_sequence(sequence) + + BaseTest.reset_controller() + + + # Test seeds generated by Electrum v4.5.5 + test_with_mnemonic("bomb congress scorpion mutual word stamp tongue valid permit salmon yellow spy".split()) + test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split()) + test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split()) + + # Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide + test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False) + test_with_mnemonic("gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split(), expects_electrum_seed_is_valid=False) + test_with_mnemonic("enough board blossom stamp fire buffalo digital solution sadness random number stone".split(), expects_electrum_seed_is_valid=False) + + def test_export_xpub_standard_flow(self): """ Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 2202af0d..f8c8917e 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -14,9 +14,9 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): assert len(mnemonic) == mnemonic_length if qr_type == QRType.SEED__SEEDQR: - e = SeedQrEncoder(seed=Seed(mnemonic)) + e = SeedQrEncoder(mnemonic=mnemonic) elif qr_type == QRType.SEED__COMPACTSEEDQR: - e = CompactSeedQrEncoder(seed=Seed(mnemonic)) + e = CompactSeedQrEncoder(mnemonic=mnemonic) data = e.next_part() print(data) From 407d1dbf75c70812a0d73cda4ff7d4478fc856f8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 13 Jul 2024 14:36:31 -0500 Subject: [PATCH 2/8] More tests; minor reformatting --- src/seedsigner/models/seed.py | 32 +++++++++++---- src/seedsigner/models/settings_definition.py | 2 + src/seedsigner/views/seed_views.py | 1 + tests/test_flows_seed.py | 5 +-- tests/test_seed.py | 42 +++++++++++++++++++- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 6164ff84..3fdd3eb2 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -42,7 +42,7 @@ class Seed: raise Exception(f"Unrecognized wordlist_language_code {wordlist_language_code}") - def _generate_seed(self) -> bool: + def _generate_seed(self): try: self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) except Exception as e: @@ -64,6 +64,7 @@ class Seed: def wordlist_language_code(self) -> str: return self._wordlist_language_code + @property def mnemonic_display_str(self) -> str: return unicodedata.normalize("NFC", " ".join(self._mnemonic)) @@ -106,29 +107,36 @@ class Seed: # TODO: Support other BIP-39 wordlist languages! raise Exception("Not yet implemented!") + @property def script_override(self) -> list: return None - + + def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: return None + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] return bip32.detect_version(derivation_path, default="xpub", network=embit_network) + @property def passphrase_label(self) -> str: return SettingsConstants.LABEL__BIP39_PASSPHRASE + @property def seedqr_supported(self) -> bool: return True + @property def bip85_supported(self) -> bool: return True + def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) return hexlify(root.child(0).fingerprint).decode('utf-8') @@ -158,19 +166,20 @@ class Seed: class ElectrumSeed(Seed): - - def _generate_seed(self) -> bool: + def _generate_seed(self): if len(self._mnemonic) != 12: - return False + raise InvalidSeedException(f"Unsupported Electrum seed length: {len(self._mnemonic)}") + s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex() prefix = s[0:3] + # only support Electrum Segwit version for now if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix: self.seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS) - return True + else: - raise InvalidSeedException("Unsupported electrum seed input") - return False + raise InvalidSeedException(f"Unsupported Electrum seed format: {prefix}") + def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): if passphrase: @@ -184,6 +193,7 @@ class ElectrumSeed(Seed): # Regenerate the internal seed since passphrase changes the result self._generate_seed() + @staticmethod def normalize_electrum_passphrase(passphrase : str) -> str: passphrase = unicodedata.normalize('NFKD', passphrase) @@ -193,25 +203,31 @@ class ElectrumSeed(Seed): passphrase = u' '.join(passphrase.split()) return passphrase + @property def script_override(self) -> list: return [SettingsConstants.NATIVE_SEGWIT] + def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h" + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] + @property def passphrase_label(self) -> str: return SettingsConstants.LABEL__CUSTOM_EXTENSION + @property def seedqr_supported(self) -> bool: return False + @property def bip85_supported(self) -> bool: return False diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index c5334018..d67dbfaf 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -209,6 +209,8 @@ class SettingsConstants: LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase" LABEL__CUSTOM_EXTENSION = "Custom Extension" + + @dataclass class SettingsEntry: """ diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 069737ec..1fb412f4 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -620,6 +620,7 @@ class SeedBackupView(View): def run(self): button_data = [self.VIEW_WORDS] + if self.seed.seedqr_supported: button_data.append(self.EXPORT_SEEDQR) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index e4fa201a..f2f5eb75 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -152,6 +152,7 @@ class TestSeedFlows(FlowTest): ] else: + # Or we bomb out if the mnemonic is invalid for Electrum sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD)) self.run_sequence(sequence) @@ -164,10 +165,8 @@ class TestSeedFlows(FlowTest): test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split()) test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split()) - # Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide + # Most BIP-39 seeds should fail; test seed generated by bitcoiner.guide test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False) - test_with_mnemonic("gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split(), expects_electrum_seed_is_valid=False) - test_with_mnemonic("enough board blossom stamp fire buffalo digital solution sadness random number stone".split(), expects_electrum_seed_is_valid=False) def test_export_xpub_standard_flow(self): diff --git a/tests/test_seed.py b/tests/test_seed.py index 04291f65..4dd8af93 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -1,8 +1,10 @@ -from seedsigner.models.seed import Seed, ElectrumSeed +import pytest +from seedsigner.models.seed import InvalidSeedException, Seed, ElectrumSeed from seedsigner.models.settings import SettingsConstants +# TODO: Change TAB indents to SPACE def test_seed(): seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()) @@ -37,9 +39,47 @@ def test_seed(): def test_electrum_seed(): + """ + ElectrumSeed should correctly parse a modern Electrum mnemonic. + """ seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split()) intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA' assert seed.seed_bytes == intended_seed + +def test_electrum_mnemonic_format(): + """ + ElectrumSeed should reject mnemonics that are not 12 words long. + """ + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic=["regular"] * 11) + + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic=["regular"] * 13) + + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic=["regular"] * 24) + + +def test_electrum_seed_rejects_most_bip39_mnemonics(): + """ + ElectrumSeed should throw an exception for most BIP-39 mnemonics. + + There are 1/16 odds that a seed will be valid for both formats. + """ + # Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic="pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split()) + + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic="gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split()) + + with pytest.raises(InvalidSeedException): + ElectrumSeed(mnemonic="enough board blossom stamp fire buffalo digital solution sadness random number stone".split()) + + # This one is valid for both formats + mnemonic = "only gain spot output unknown craft simple cram absorb suggest ridge famous".split() + Seed(mnemonic) + ElectrumSeed(mnemonic) From bede64dd4f62b6fa8a3ad3ea22b5cc2d6423360c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 13 Jul 2024 14:45:17 -0500 Subject: [PATCH 3/8] Update test_seedqr.py --- tests/test_seedqr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index f8c8917e..d792683c 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -19,7 +19,6 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): e = CompactSeedQrEncoder(mnemonic=mnemonic) data = e.next_part() - print(data) qr = QR() image = qr.qrimage( From 8c17ef1b637bdee376936cca631d1aa6d5794416 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 13 Jul 2024 15:00:08 -0500 Subject: [PATCH 4/8] Additional Settings screenshot --- tests/screenshot_generator/generator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index e98c6541..4c0c092a 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -134,6 +134,16 @@ def test_generate_screenshots(target_locale): # Automatically populate all Settings options Views settings_views_list = [] settings_views_list.append(settings_views.SettingsMenuView) + settings_views_list.append(( + settings_views.SettingsMenuView, + dict( + visibility=SettingsConstants.VISIBILITY__ADVANCED, + selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS, + initial_scroll=240, # Just guessing how many pixels to scroll down + ), + "SettingsMenuView__Advanced" + )) + # so we get a choice for transcribe seed qr format controller.settings.set_value( attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR, From 3b50309a9e69371e2ccc18eb72002053d2fbd557 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 14 Jul 2024 08:51:23 -0500 Subject: [PATCH 5/8] Routing bugfixes, additional tests --- src/seedsigner/helpers/embit_utils.py | 1 + src/seedsigner/models/seed.py | 18 ++++----- src/seedsigner/views/psbt_views.py | 5 +-- src/seedsigner/views/seed_views.py | 12 +++++- src/seedsigner/views/tools_views.py | 6 +-- tests/test_flows_psbt.py | 45 ++++++++++++++++++++++- tests/test_flows_seed.py | 53 +++++++++++++++++++++------ tests/test_flows_tools.py | 41 ++++++++++++++++++--- 8 files changed, 147 insertions(+), 34 deletions(-) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index b0ba9fd1..a2b8c850 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -19,6 +19,7 @@ from seedsigner.models.settings_definition import SettingsConstants # TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`? +# TODO: Refactor `wallet_type` to conform to our `sig_type` naming convention def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str: if network == SettingsConstants.MAINNET: network_path = "0'" diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 3fdd3eb2..a93f6366 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -109,15 +109,15 @@ class Seed: @property - def script_override(self) -> list: + def script_override(self) -> str: return None - def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str: return None - def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] return bip32.detect_version(derivation_path, default="xpub", network=embit_network) @@ -205,17 +205,17 @@ class ElectrumSeed(Seed): @property - def script_override(self) -> list: - return [SettingsConstants.NATIVE_SEGWIT] + def script_override(self) -> str: + return SettingsConstants.NATIVE_SEGWIT - def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: - return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h" + def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str: + return "m/0h" if sig_type == SettingsConstants.SINGLE_SIG else "m/1h" - def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] - return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] + return embit_network["zpub"] if sig_type == SettingsConstants.SINGLE_SIG else embit_network["Zpub"] @property diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 16822882..9eaa6ae0 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -79,9 +79,8 @@ class PSBTSelectSeedView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - from seedsigner.views.seed_views import SeedMnemonicEntryView - self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) - return Destination(SeedMnemonicEntryView) + from seedsigner.views.seed_views import SeedElectrumMnemonicStartView + return Destination(SeedElectrumMnemonicStartView) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 1fb412f4..9cc336c7 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -688,8 +688,16 @@ class SeedExportXpubScriptTypeView(View): def run(self): from .tools_views import ToolsAddressExplorerAddressTypeView args = {"seed_num": self.seed_num, "sig_type": self.sig_type} + + script_types = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) + seed = self.controller.storage.seeds[self.seed_num] - script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) + if seed.script_override: + # This seed only allows one script type + # TODO: Does it matter if the Settings don't have the override script type + # enabled? + script_types = [seed.script_override] + if len(script_types) == 1: # Nothing to select; skip this screen args["script_type"] = script_types[0] @@ -1729,7 +1737,7 @@ class SeedAddressVerificationView(View): raise Exception("Can't validate a single sig addr without specifying a seed") self.seed_num = seed_num self.seed = self.controller.get_seed(seed_num) - self.seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG) + self.seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG) else: self.seed = None self.address = self.controller.unverified_address["address"] diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index fa908891..f6d29114 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -496,8 +496,8 @@ class ToolsAddressExplorerSelectSourceView(View): return Destination(SeedMnemonicEntryView) elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: - from seedsigner.views.seed_views import SeedElectrumWarningView - return Destination(SeedElectrumWarningView) + from seedsigner.views.seed_views import SeedElectrumMnemonicStartView + return Destination(SeedElectrumMnemonicStartView) @@ -532,7 +532,7 @@ class ToolsAddressExplorerAddressTypeView(View): if self.seed_num is not None: self.seed = self.controller.storage.seeds[seed_num] data["seed_num"] = self.seed - seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG) + seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG) if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index c328fe24..59237cf0 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -1,5 +1,6 @@ from base import FlowTest, FlowStep +from seedsigner.controller import Controller from seedsigner.views.view import MainMenuView from seedsigner.views import scan_views, seed_views, psbt_views from seedsigner.models.settings import SettingsConstants @@ -57,7 +58,49 @@ class TestPSBTFlows(FlowTest): FlowStep(MainMenuView) ]) - + + def test_scan_psbt_first_then_load_electrum_seed(self): + """ + Should be able to load an Electrum mnemonic after first loading in a psbt. + """ + def load_psbt_into_decoder(view: scan_views.ScanView): + # Same psbt as above, but we don't care about the details here + view.decoder.add_data("cHNidP8BANgCAAAAAsTXZs3fz/dmGb6M80+jjvJZdYya+cw5bT/dGuhZFdSlAAAAAAD9////qo6xg/UZAvUkcbse1F+C9zbP/FeZNjThx7SCIn6eMCgBAAAAAP3///8EQOIBAAAAAAAWABSkZPM7kLcTRE2En1t33/0RCHgMjQXYnnYAAAAAFgAUKMaPRKXdY4m8iKrE9j+rycskJU1A4gEAAAAAABYAFPYc9wiHRrYKAZYLLztREAwpPBIwipVcAwAAAAAWABSiFuiJIa4NrxLUBVQNS0NIun6DDtoRAABPAQQ1h88DBcQGZIAAAAA+0J+jlNL3dpWwlnBi8Dx+Ipg4e6uvB3HdjzFPX7r9CAOOlAIxgII+/xCcj+XoEenKH7wj5s5wlu7Q7CCZWFLGLhA5Su0UVAAAgAEAAIAAAACAAAEA7QIAAAAEE6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0BAAAAAP3///8TqeNf9+e+fuFuQohFzHM1gU5J9sJ015ad3sV7/VRxDQMAAAAA/f///xOp41/3575+4W5CiEXMczWBTkn2wnTXlp3exXv9VHENBAAAAAD9////E6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0GAAAAAP3///8CUnheAwAAAAAWABRCfygPJ+Fjsx4BknYvvm3A3qKn2xJ/XQcAAAAAF6kU1I4TAst5nAj15ey7vwe5cM3OFq+HlhEAAAEBH1J4XgMAAAAAFgAUQn8oDyfhY7MeAZJ2L75twN6ip9sBAwQBAAAAIgYCo7sfm78RQY3B5n0ac/QF8VtMAzFnci+h5D1MtpgRY7oYOUrtFFQAAIABAACAAAAAgAEAAAAGAAAAAAEAcQIAAAABxY7wh0nsfJQfzWrD/9rN9BYsM+iOmPaO6I0ANFgO/PcAAAAAAP3///8CptiUAAAAAAAWABRIm4HhQY/TzOjeWSPRrbuJo9MlW826oHYAAAAAFgAU0z+0L2QSLGtyQTn8FhbCpcI7jbliAQAAAQEfzbqgdgAAAAAWABTTP7QvZBIsa3JBOfwWFsKlwjuNuQEDBAEAAAAiBgITHmebEANk81CraV4xZIpqkNjjw0tIvezl1Ism1NRH3Rg5Su0UVAAAgAEAAIAAAACAAQAAAAAAAAAAIgICuTT7WnuiUTpObjWnZFHzIeEvW9PTB+1LLVFNQJVFeIIYOUrtFFQAAIABAACAAAAAgAEAAAAHAAAAACICAk8f3hpc5C35chgSg+Pe2zZ9IhHREd4aKW2+yAMRIFeqGDlK7RRUAACAAQAAgAAAAIABAAAACQAAAAAAIgIDjt1CjvrnMMnjbmTNKUAYoKEDRbmKjNjbq+6Ppqj3bqQYOUrtFFQAAIABAACAAAAAgAEAAAAIAAAAAA==") + + self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED) + + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.TYPE_ELECTRUM), + FlowStep(seed_views.SeedElectrumMnemonicStartView), + ] + + # Load an Electrum mnemonic during the flow (same one used in test_seed.py) + # This seed can't actually sign the psbt. + for word in "regular reject rare profit once math fringe chase until ketchup century escape".split(): + sequence += [ + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word), + ] + + sequence += [ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + + # TODO: Change the PSBT to one that this Electrum seed can actually sign in + # order to test the full flow: + # FlowStep(seed_views.SeedOptionsView, is_redirect=True), + # FlowStep(psbt_views.PSBTOverviewView), + + # Until then, the View won't actually auto-route us back into the PSBT flow + FlowStep(seed_views.SeedOptionsView), + ] + + self.run_sequence(sequence) + + # But we can at least verify that we're still in the PSBT flow + assert self.controller.resume_main_flow == Controller.FLOW__PSBT + + def test_scan_multisig_psbt_seed_already_signed_flow(self): def load_psbt_into_decoder(view: scan_views.ScanView): diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index f2f5eb75..7f7a2f0d 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -3,11 +3,11 @@ import pytest # Must import test base before the Controller from base import BaseTest, FlowTest, FlowStep -from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException +from base import FlowTestInvalidButtonDataSelectionException from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON from seedsigner.models.settings import Settings, SettingsConstants -from seedsigner.models.seed import Seed +from seedsigner.models.seed import ElectrumSeed, Seed from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView, NotYetImplementedView from seedsigner.views import seed_views, scan_views, settings_views, tools_views @@ -243,15 +243,15 @@ class TestSeedFlows(FlowTest): self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script]) self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord]) - # test that multisig is not an option via exception raised when redirected to next step instead of having a choice - with pytest.raises(FlowTestRunScreenNotExecutedException) as e: - self.run_sequence( - initial_destination_view_args=dict(seed_num=0), - sequence=[ - FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), - FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig), - ] - ) + # If multisig isn't an option, then the sig type selection is skipped altogether + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView), + ] + ) # test that taproot is not an option via exception raised when choice is taproot with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e: @@ -345,6 +345,37 @@ class TestSeedFlows(FlowTest): ) + def test_export_xpub_electrum_seed_flow(self): + """ + Electrum seeds should skip script type selection + """ + # Load a finalized Seed into the Controller + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + self.controller.storage.set_pending_seed(ElectrumSeed("regular reject rare profit once math fringe chase until ketchup century escape".split())) + self.controller.storage.finalize_pending_seed() + + # Make sure all options are enabled + self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in SettingsConstants.ALL_SIG_TYPES]) + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in SettingsConstants.ALL_SCRIPT_TYPES]) + self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in SettingsConstants.ALL_COORDINATORS]) + + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=seed_views.SeedExportXpubSigTypeView.SINGLE_SIG), + + # Skips past the script type options via redirect + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0]), + FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), + FlowStep(MainMenuView), + ] + ) + + def test_discard_seed_flow(self): """ Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py index 38d8325c..588859be 100644 --- a/tests/test_flows_tools.py +++ b/tests/test_flows_tools.py @@ -39,8 +39,6 @@ class TestToolsFlows(FlowTest): Finalizing a seed during the Address Explorer flow should return to the next Address Explorer step upon completion. """ - controller = Controller.get_instance() - def load_seed_into_decoder(view: scan_views.ScanView): view.decoder.add_data("0000" * 11 + "0003") @@ -55,11 +53,11 @@ class TestToolsFlows(FlowTest): FlowStep(seed_views.SeedExportXpubScriptTypeView), ]) - assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER + assert self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER # Reset - controller.storage.seeds.clear() - controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"])) + self.controller.storage.seeds.clear() + self.controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"])) # Finalize the new seed w/passphrase self.run_sequence( @@ -73,6 +71,39 @@ class TestToolsFlows(FlowTest): ) + def test__address_explorer__load_electrum_seed__sideflow(self): + """ + Loading an Electrum seed during the Address Explorer flow should return to + the Address Explorer flow upon completion, skip the script type selection, + and successfully generate receive or change addresses. + """ + self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED) + + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.ADDRESS_EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.TYPE_ELECTRUM), + FlowStep(seed_views.SeedElectrumMnemonicStartView), + ] + + # Load an Electrum mnemonic during the flow (same one used in test_seed.py) + for word in "regular reject rare profit once math fringe chase until ketchup century escape".split(): + sequence += [ + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word), + ] + + sequence += [ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE), + FlowStep(tools_views.ToolsAddressExplorerAddressListView), + ] + + self.run_sequence(sequence) + + + def test__address_explorer__scan_wrong_qrtype__flow(self): """ Scanning the wrong type of QR code when a SeedQR is expected should route to ErrorView From b71acb6fa4400a2a112a76d172a25a5b0516017a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 14 Jul 2024 10:28:00 -0500 Subject: [PATCH 6/8] Adds valid Electrum seed + spendable psbt courtesy of @newtonick! --- tests/test_flows_psbt.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 59237cf0..964b0d67 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -64,8 +64,8 @@ class TestPSBTFlows(FlowTest): Should be able to load an Electrum mnemonic after first loading in a psbt. """ def load_psbt_into_decoder(view: scan_views.ScanView): - # Same psbt as above, but we don't care about the details here - view.decoder.add_data("cHNidP8BANgCAAAAAsTXZs3fz/dmGb6M80+jjvJZdYya+cw5bT/dGuhZFdSlAAAAAAD9////qo6xg/UZAvUkcbse1F+C9zbP/FeZNjThx7SCIn6eMCgBAAAAAP3///8EQOIBAAAAAAAWABSkZPM7kLcTRE2En1t33/0RCHgMjQXYnnYAAAAAFgAUKMaPRKXdY4m8iKrE9j+rycskJU1A4gEAAAAAABYAFPYc9wiHRrYKAZYLLztREAwpPBIwipVcAwAAAAAWABSiFuiJIa4NrxLUBVQNS0NIun6DDtoRAABPAQQ1h88DBcQGZIAAAAA+0J+jlNL3dpWwlnBi8Dx+Ipg4e6uvB3HdjzFPX7r9CAOOlAIxgII+/xCcj+XoEenKH7wj5s5wlu7Q7CCZWFLGLhA5Su0UVAAAgAEAAIAAAACAAAEA7QIAAAAEE6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0BAAAAAP3///8TqeNf9+e+fuFuQohFzHM1gU5J9sJ015ad3sV7/VRxDQMAAAAA/f///xOp41/3575+4W5CiEXMczWBTkn2wnTXlp3exXv9VHENBAAAAAD9////E6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0GAAAAAP3///8CUnheAwAAAAAWABRCfygPJ+Fjsx4BknYvvm3A3qKn2xJ/XQcAAAAAF6kU1I4TAst5nAj15ey7vwe5cM3OFq+HlhEAAAEBH1J4XgMAAAAAFgAUQn8oDyfhY7MeAZJ2L75twN6ip9sBAwQBAAAAIgYCo7sfm78RQY3B5n0ac/QF8VtMAzFnci+h5D1MtpgRY7oYOUrtFFQAAIABAACAAAAAgAEAAAAGAAAAAAEAcQIAAAABxY7wh0nsfJQfzWrD/9rN9BYsM+iOmPaO6I0ANFgO/PcAAAAAAP3///8CptiUAAAAAAAWABRIm4HhQY/TzOjeWSPRrbuJo9MlW826oHYAAAAAFgAU0z+0L2QSLGtyQTn8FhbCpcI7jbliAQAAAQEfzbqgdgAAAAAWABTTP7QvZBIsa3JBOfwWFsKlwjuNuQEDBAEAAAAiBgITHmebEANk81CraV4xZIpqkNjjw0tIvezl1Ism1NRH3Rg5Su0UVAAAgAEAAIAAAACAAQAAAAAAAAAAIgICuTT7WnuiUTpObjWnZFHzIeEvW9PTB+1LLVFNQJVFeIIYOUrtFFQAAIABAACAAAAAgAEAAAAHAAAAACICAk8f3hpc5C35chgSg+Pe2zZ9IhHREd4aKW2+yAMRIFeqGDlK7RRUAACAAQAAgAAAAIABAAAACQAAAAAAIgIDjt1CjvrnMMnjbmTNKUAYoKEDRbmKjNjbq+6Ppqj3bqQYOUrtFFQAAIABAACAAAAAgAEAAAAIAAAAAA==") + # Single sig psbt for the below Electrum mnemonic + view.decoder.add_data("cHNidP8BAHECAAAAAX9/d6VyI7nvVTyhLBfqu05za2AJ2Z0dKMC0cUX+S2U7AQAAAAD9////AgeHAAAAAAAAFgAUOnNPuZMD1sQudt3+7LvHBUvGhyd//gAAAAAAABYAFGO9QLvu4V9/hz6ZjbIGMrqsEiIYAjQTAAABAR+ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAQDBAQAAAAABAYeHL9UQlz/jEKUuNNY3LTeQRjudjBinsP2L0ppvgRt0AAAAAAD/////AnbP3rsPAAAAIlEgtgmCioGjfKwp6f8rOoI4OPb+ZV8db581J9IizZPskl2ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAUDCBlMh9VjZN2NdU9Wabi0o3Ct1q9YHTsJRLAkLfUuIHB+BE+ucR4bdGAJG5nBhCWOmCXbpRwKP1INRYvkuQ2fHAAAAACIGA2+PEYHyVy6nhYwAx5SJKBIWXjsWgjhhf/2FEWqXgxnoEKNOC3gAAACAAAAAAAAAAAAAACICA0SBeeHxfHdny6rUnQJuteAnQ7shSydexjJCkSJarn3mEKNOC3gAAACAAQAAAAEAAAAA") self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED) @@ -76,30 +76,21 @@ class TestPSBTFlows(FlowTest): FlowStep(seed_views.SeedElectrumMnemonicStartView), ] - # Load an Electrum mnemonic during the flow (same one used in test_seed.py) - # This seed can't actually sign the psbt. - for word in "regular reject rare profit once math fringe chase until ketchup century escape".split(): + # Load the associated Electrum mnemonic during the flow + for word in "apple drip silly junior language resource unaware whale snake copy gravity tank".split(): sequence += [ FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word), ] sequence += [ FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), - - # TODO: Change the PSBT to one that this Electrum seed can actually sign in - # order to test the full flow: - # FlowStep(seed_views.SeedOptionsView, is_redirect=True), - # FlowStep(psbt_views.PSBTOverviewView), - - # Until then, the View won't actually auto-route us back into the PSBT flow - FlowStep(seed_views.SeedOptionsView), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), ] self.run_sequence(sequence) - # But we can at least verify that we're still in the PSBT flow - assert self.controller.resume_main_flow == Controller.FLOW__PSBT - def test_scan_multisig_psbt_seed_already_signed_flow(self): From 134476f1f7113fa8fd72b1c0014cebcb4a70263d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 14 Jul 2024 12:58:49 -0500 Subject: [PATCH 7/8] Add Electrum "custom extension" route to FlowTest --- src/seedsigner/models/settings_definition.py | 2 +- tests/test_flows_seed.py | 22 ++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index d67dbfaf..c96bd68b 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -207,7 +207,7 @@ class SettingsConstants: # Label strings LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase" - LABEL__CUSTOM_EXTENSION = "Custom Extension" + LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 7f7a2f0d..ae602fc8 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -126,7 +126,7 @@ class TestSeedFlows(FlowTest): Most BIP-39 mnemonics should generate an error if entered as Electrum seeds. """ - def test_with_mnemonic(mnemonic: list[str], expects_electrum_seed_is_valid: bool = True): + def test_with_mnemonic(mnemonic: list[str], custom_extension: str = None, expects_electrum_seed_is_valid: bool = True): Settings.HOSTNAME = "not seedsigner-os" settings = Settings.get_instance() settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED) @@ -143,13 +143,21 @@ class TestSeedFlows(FlowTest): sequence.append( FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word) ) - + if expects_electrum_seed_is_valid: # With the mnemonic completely entered, we land on the SeedFinalizeView - sequence += [ - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), - FlowStep(seed_views.SeedOptionsView), - ] + if custom_extension: + sequence += [ + FlowStep(seed_views.SeedFinalizeView, screen_return_value=1), # The passphrase / custom extension button is dynamic so there's no constant to refer to here + FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=custom_extension), + FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), + FlowStep(seed_views.SeedOptionsView), + ] + else: + sequence += [ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView), + ] else: # Or we bomb out if the mnemonic is invalid for Electrum @@ -164,6 +172,8 @@ class TestSeedFlows(FlowTest): test_with_mnemonic("bomb congress scorpion mutual word stamp tongue valid permit salmon yellow spy".split()) test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split()) test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split()) + test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="test") + test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="monkey fling orange coin good") # Most BIP-39 seeds should fail; test seed generated by bitcoiner.guide test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False) From 6d114f7ab113156ca3cda1707f328d63d0a78fdd Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 14 Jul 2024 13:23:53 -0500 Subject: [PATCH 8/8] Fix to accomodate changes from #563 --- tests/test_flows_seed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index ae602fc8..7171ef87 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -149,7 +149,7 @@ class TestSeedFlows(FlowTest): if custom_extension: sequence += [ FlowStep(seed_views.SeedFinalizeView, screen_return_value=1), # The passphrase / custom extension button is dynamic so there's no constant to refer to here - FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=custom_extension), + FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase=custom_extension)), # This is a one-off oddity where the Screen returns dict instead of int | str FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), FlowStep(seed_views.SeedOptionsView), ]