Proper View separation; allows for more screenshots; button_data bugfix

This commit is contained in:
kdmukai
2025-02-01 17:04:57 -06:00
parent 4a67f94f70
commit ddba341f67
4 changed files with 158 additions and 54 deletions
+100 -52
View File
@@ -850,7 +850,6 @@ class SeedExportXpubCoordinatorView(View):
class SeedExportXpubWarningView(View): class SeedExportXpubWarningView(View):
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str): def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
super().__init__() super().__init__()
@@ -1196,6 +1195,7 @@ class SeedBIP85SelectChildIndexView(View):
) )
class SeedBIP85InvalidChildIndexView(View): class SeedBIP85InvalidChildIndexView(View):
def __init__(self, seed_num: int, num_words: int): def __init__(self, seed_num: int, num_words: int):
super().__init__() super().__init__()
@@ -1419,6 +1419,14 @@ class SeedWordsBackupTestSuccessView(View):
Export as SeedQR Export as SeedQR
****************************************************************************""" ****************************************************************************"""
class SeedTranscribeSeedQRFormatView(View): class SeedTranscribeSeedQRFormatView(View):
# SeedQR dims for 12-word seeds
STANDARD_12 = ButtonOption("Standard: 25x25", return_data=25)
COMPACT_12 = ButtonOption("Compact: 21x21", return_data=21)
# SeedQR dims for 24-word seeds
STANDARD_24 = ButtonOption("Standard: 29x29", return_data=29)
COMPACT_24 = ButtonOption("Compact: 25x25", return_data=25)
def __init__(self, seed_num: int): def __init__(self, seed_num: int):
super().__init__() super().__init__()
self.seed_num = seed_num self.seed_num = seed_num
@@ -1426,12 +1434,6 @@ class SeedTranscribeSeedQRFormatView(View):
def run(self): def run(self):
seed = self.controller.get_seed(self.seed_num) seed = self.controller.get_seed(self.seed_num)
if len(seed.mnemonic_list) == 12:
STANDARD = ButtonOption("Standard: 25x25", return_data=25)
COMPACT = ButtonOption("Compact: 21x21", return_data=21)
else:
STANDARD = ButtonOption("Standard: 29x29", return_data=29)
COMPACT = ButtonOption("Compact: 25x25", return_data=25)
if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED:
# Only configured for standard SeedQR # Only configured for standard SeedQR
@@ -1440,22 +1442,26 @@ class SeedTranscribeSeedQRFormatView(View):
view_args={ view_args={
"seed_num": self.seed_num, "seed_num": self.seed_num,
"seedqr_format": QRType.SEED__SEEDQR, "seedqr_format": QRType.SEED__SEEDQR,
"num_modules": STANDARD.return_data, "num_modules": self.STANDARD_12.return_data,
}, },
skip_current_view=True, skip_current_view=True,
) )
button_data = [STANDARD, COMPACT] if len(seed.mnemonic_list) == 12:
button_data = [self.STANDARD_12, self.COMPACT_12]
else:
button_data = [self.STANDARD_24, self.COMPACT_24]
selected_menu_num = seed_screens.SeedTranscribeSeedQRFormatScreen( selected_menu_num = self.run_screen(
seed_screens.SeedTranscribeSeedQRFormatScreen,
title=_("SeedQR Format"), title=_("SeedQR Format"),
button_data=button_data, button_data=button_data,
).display() )
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
if button_data[selected_menu_num] == STANDARD: if button_data[selected_menu_num] in [self.STANDARD_12, self.STANDARD_24]:
seedqr_format = QRType.SEED__SEEDQR seedqr_format = QRType.SEED__SEEDQR
else: else:
seedqr_format = QRType.SEED__COMPACTSEEDQR seedqr_format = QRType.SEED__COMPACTSEEDQR
@@ -1496,10 +1502,11 @@ class SeedTranscribeSeedQRWarningView(View):
# Forward straight to transcribing the SeedQR # Forward straight to transcribing the SeedQR
return destination return destination
selected_menu_num = DireWarningScreen( selected_menu_num = self.run_screen(
DireWarningScreen,
status_headline=_("SeedQR is your private key!"), status_headline=_("SeedQR is your private key!"),
text=_("Never photograph or scan it into a device that connects to the internet."), text=_("Never photograph or scan it into a device that connects to the internet."),
).display() )
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
@@ -1529,10 +1536,11 @@ class SeedTranscribeSeedQRWholeQRView(View):
data = e.next_part() data = e.next_part()
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen( ret = self.run_screen(
seed_screens.SeedTranscribeSeedQRWholeQRScreen,
qr_data=data, qr_data=data,
num_modules=self.num_modules, num_modules=self.num_modules,
).display() )
if ret == RET_CODE__BACK_BUTTON: if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
@@ -1607,10 +1615,11 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View):
def run(self): def run(self):
button_data = [self.SCAN, self.DONE] button_data = [self.SCAN, self.DONE]
selected_menu_option = seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen( selected_menu_option = self.run_screen(
seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen,
title=_("Confirm SeedQR?"), title=_("Confirm SeedQR?"),
button_data=button_data, button_data=button_data,
).display() )
if selected_menu_option == RET_CODE__BACK_BUTTON: if selected_menu_option == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
@@ -1625,61 +1634,100 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View):
class SeedTranscribeSeedQRConfirmScanView(View): class SeedTranscribeSeedQRConfirmScanView(View):
def __init__(self, seed_num: int): def __init__(self, seed_num: int):
from seedsigner.models.decode_qr import DecodeQR
super().__init__() super().__init__()
self.seed_num = seed_num self.seed_num = seed_num
self.seed = self.controller.get_seed(seed_num) self.seed = self.controller.get_seed(seed_num)
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
def run(self): def run(self):
from seedsigner.gui.screens.scan_screens import ScanScreen from seedsigner.gui.screens.scan_screens import ScanScreen
from seedsigner.models.decode_qr import DecodeQR
# Run the live preview and QR code capture process # Run the live preview and QR code capture process
# TODO: Does this belong in its own BaseThread? # TODO: Does this belong in its own BaseThread?
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) self.run_screen(
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code) ScanScreen,
ScanScreen(
decoder=self.decoder, decoder=self.decoder,
instructions_text=_("Scan your SeedQR") instructions_text=_("Scan your SeedQR")
).display() )
if self.decoder.is_complete: if self.decoder.is_complete:
if self.decoder.is_seed: if self.decoder.is_seed:
seed_mnemonic = self.decoder.get_seed_phrase() seed_mnemonic = self.decoder.get_seed_phrase()
# Found a valid mnemonic seed! But does it match? # Found a valid mnemonic seed! But does it match?
if seed_mnemonic != self.seed.mnemonic_list: if seed_mnemonic != self.seed.mnemonic_list:
DireWarningScreen( return Destination(SeedTranscribeSeedQRConfirmWrongSeedView, skip_current_view=True)
title=_("Confirm SeedQR"),
status_headline=_("Error!"),
text=_("Your transcribed SeedQR does not match your original seed!"),
show_back_button=False,
button_data=[_("Review SeedQR")],
).display()
return Destination(BackStackView, skip_current_view=True)
else: else:
from seedsigner.gui.screens.screen import LargeIconStatusScreen return Destination(SeedTranscribeSeedQRConfirmSuccessView, view_args={"seed_num": self.seed_num})
LargeIconStatusScreen(
title=_("Confirm SeedQR"),
status_headline=_("Success!"),
text=_("Your transcribed SeedQR successfully scanned and yielded the same seed."),
show_back_button=False,
button_data=[_("OK")],
).display()
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}) else:
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True)
else:
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
DireWarningScreen(
title=_("Confirm SeedQR"),
status_headline=_("Error!"),
text=_("Your transcribed SeedQR could not be read!"),
show_back_button=False,
button_data=[_("Review SeedQR")],
).display()
return Destination(BackStackView, skip_current_view=True)
class SeedTranscribeSeedQRConfirmWrongSeedView(View):
"""
A valid SeedQR was scanned but it did NOT match the one we just transcribed!
"""
def run(self):
self.run_screen(
DireWarningScreen,
title=_("Confirm SeedQR"),
status_headline=_("Error!"),
text=_("Your transcribed SeedQR does not match your original seed!"),
show_back_button=False,
button_data=[ButtonOption("Review SeedQR")],
)
# Skip BACK to the zoomed in transcription view
return Destination(BackStackView, skip_current_view=True)
class SeedTranscribeSeedQRConfirmInvalidQRView(View):
"""
A QR code was scanned but it was not a SeedQR and certainly not the SeedQR we just
transcribed!
"""
def run(self):
# TODO: A better error message would be something like: "The QR code you scanned does not contain a valid SeedQR."
self.run_screen(
DireWarningScreen,
title=_("Confirm SeedQR"),
status_headline=_("Error!"),
text=_("Your transcribed SeedQR could not be read!"),
show_back_button=False,
button_data=[ButtonOption("Review SeedQR")],
)
# Skip BACK to the zoomed in transcription view
return Destination(BackStackView, skip_current_view=True)
class SeedTranscribeSeedQRConfirmSuccessView(View):
"""
The SeedQR we just scanned matched the one we just transcribed.
"""
def __init__(self, seed_num: int):
super().__init__()
self.seed_num = seed_num
def run(self):
from seedsigner.gui.screens.screen import LargeIconStatusScreen
self.run_screen(
LargeIconStatusScreen,
title=_("Confirm SeedQR"),
status_headline=_("Success!"),
text=_("Your transcribed SeedQR successfully scanned and yielded the same seed."),
show_back_button=False,
button_data=[ButtonOption("OK")],
)
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
+13 -1
View File
@@ -13,7 +13,7 @@ sys.modules['seedsigner.hardware.buttons'] = MagicMock()
sys.modules['seedsigner.hardware.camera'] = MagicMock() sys.modules['seedsigner.hardware.camera'] = MagicMock()
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON, ButtonOption
from seedsigner.hardware.microsd import MicroSD from seedsigner.hardware.microsd import MicroSD
from seedsigner.models.settings import Settings from seedsigner.models.settings import Settings
from seedsigner.views.view import Destination, MainMenuView, UnhandledExceptionView, View from seedsigner.views.view import Destination, MainMenuView, UnhandledExceptionView, View
@@ -140,6 +140,12 @@ class FlowStep:
class FlowTestInvalidButtonDataInstanceTypeException(FlowBasedTestException):
""" The button_data contained an item that was not a ButtonOption instance """
pass
class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException): class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException):
""" The FlowStep's button_data_selection value was not found in the View's button_data """ """ The FlowStep's button_data_selection value was not found in the View's button_data """
pass pass
@@ -250,6 +256,12 @@ class FlowTest(BaseTest):
""" """
cur_flow_step = sequence[0] cur_flow_step = sequence[0]
if "button_data" in kwargs:
# Verify that they are all proper ButtonOption instances
for button_option in kwargs.get("button_data"):
if not isinstance(button_option, ButtonOption):
raise FlowTestInvalidButtonDataInstanceTypeException(f"button_data must be a list of ButtonOption instances, not {type(button_option)}: {button_option}")
if cur_flow_step.button_data_selection: if cur_flow_step.button_data_selection:
# We're mocking out the View.run_screen() method, so we'll get all of the # We're mocking out the View.run_screen() method, so we'll get all of the
# input args that are normally passed into the Screen.run() method, # input args that are normally passed into the Screen.run() method,
+3
View File
@@ -290,6 +290,9 @@ def generate_screenshots(locale):
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"), ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmSuccessView, dict(seed_num=0)),
# Screenshot can't render live preview screens # Screenshot can't render live preview screens
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), # ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),
+42 -1
View File
@@ -1,4 +1,5 @@
from typing import Callable from typing import Callable
from unittest.mock import patch
import pytest import pytest
# Must import test base before the Controller # Must import test base before the Controller
@@ -8,7 +9,7 @@ from base import FlowTestInvalidButtonDataSelectionException
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.seed import ElectrumSeed, Seed from seedsigner.models.seed import ElectrumSeed, Seed
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, View, NetworkMismatchErrorView from seedsigner.views.view import MainMenuView, OptionDisabledView, View, NetworkMismatchErrorView
from seedsigner.views import seed_views, scan_views, settings_views from seedsigner.views import seed_views, scan_views, settings_views
@@ -419,6 +420,45 @@ class TestSeedFlows(FlowTest):
) )
@patch("seedsigner.gui.screens.seed_screens.SeedTranscribeSeedQRZoomedInScreen", autospec=True)
def test_transcribe_seedqr_and_verify(self, mock_zoomed_in_screen: Callable):
"""
"""
# Load a finalized Seed into the Controller
mnemonic = ["abandon"] * 11 + ["about"]
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
self.controller.storage.finalize_pending_seed()
def load_wrong_seed_into_decoder(view: View):
view.decoder.add_data("0138" * 24)
def load_right_seed_into_decoder(view: View):
view.decoder.add_data("0000" * 11 + "0003")
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
FlowStep(seed_views.SeedsMenuView, screen_return_value=0),
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.BACKUP),
FlowStep(seed_views.SeedBackupView, button_data_selection=seed_views.SeedBackupView.EXPORT_SEEDQR),
FlowStep(seed_views.SeedTranscribeSeedQRFormatView, button_data_selection=seed_views.SeedTranscribeSeedQRFormatView.STANDARD_12),
FlowStep(seed_views.SeedTranscribeSeedQRWarningView),
FlowStep(seed_views.SeedTranscribeSeedQRWholeQRView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are a bit weird; not sure why `is_redirect` is necessary here
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
# Intentionally "scan" the wrong SeedQR
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_wrong_seed_into_decoder),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are still weird
# Now scan the correct SeedQR
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_right_seed_into_decoder),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmSuccessView),
FlowStep(seed_views.SeedOptionsView),
])
class TestMessageSigningFlows(FlowTest): class TestMessageSigningFlows(FlowTest):
MAINNET_DERIVATION_PATH = "m/84h/0h/0h/0/0" MAINNET_DERIVATION_PATH = "m/84h/0h/0h/0/0"
@@ -675,3 +715,4 @@ class TestMessageSigningFlows(FlowTest):
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET) self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET)
expect_unsupported_derivation(self.load_custom_derivation_into_decoder) expect_unsupported_derivation(self.load_custom_derivation_into_decoder)