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