Skip to content

Commit c8a8b9f

Browse files
authored
Merge pull request #677 from kdmukai/transcribe_seedqr_verify_bugfix
[Bugfix] Verification Views after transcribing SeedQR fixes, refactors, test, screenshots
2 parents 4a67f94 + e028b18 commit c8a8b9f

File tree

6 files changed

+207
-59
lines changed

6 files changed

+207
-59
lines changed

src/seedsigner/views/seed_views.py

+101-53
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,6 @@ def run(self):
850850

851851

852852

853-
854853
class SeedExportXpubWarningView(View):
855854
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
856855
super().__init__()
@@ -1196,6 +1195,7 @@ def run(self):
11961195
)
11971196

11981197

1198+
11991199
class SeedBIP85InvalidChildIndexView(View):
12001200
def __init__(self, seed_num: int, num_words: int):
12011201
super().__init__()
@@ -1419,19 +1419,21 @@ def run(self):
14191419
Export as SeedQR
14201420
****************************************************************************"""
14211421
class SeedTranscribeSeedQRFormatView(View):
1422+
# SeedQR dims for 12-word seeds
1423+
STANDARD_12 = ButtonOption("Standard: 25x25", return_data=25)
1424+
COMPACT_12 = ButtonOption("Compact: 21x21", return_data=21)
1425+
1426+
# SeedQR dims for 24-word seeds
1427+
STANDARD_24 = ButtonOption("Standard: 29x29", return_data=29)
1428+
COMPACT_24 = ButtonOption("Compact: 25x25", return_data=25)
1429+
14221430
def __init__(self, seed_num: int):
14231431
super().__init__()
14241432
self.seed_num = seed_num
14251433

14261434

14271435
def run(self):
14281436
seed = self.controller.get_seed(self.seed_num)
1429-
if len(seed.mnemonic_list) == 12:
1430-
STANDARD = ButtonOption("Standard: 25x25", return_data=25)
1431-
COMPACT = ButtonOption("Compact: 21x21", return_data=21)
1432-
else:
1433-
STANDARD = ButtonOption("Standard: 29x29", return_data=29)
1434-
COMPACT = ButtonOption("Compact: 25x25", return_data=25)
14351437

14361438
if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED:
14371439
# Only configured for standard SeedQR
@@ -1440,22 +1442,26 @@ def run(self):
14401442
view_args={
14411443
"seed_num": self.seed_num,
14421444
"seedqr_format": QRType.SEED__SEEDQR,
1443-
"num_modules": STANDARD.return_data,
1445+
"num_modules": self.STANDARD_12.return_data,
14441446
},
14451447
skip_current_view=True,
14461448
)
14471449

1448-
button_data = [STANDARD, COMPACT]
1450+
if len(seed.mnemonic_list) == 12:
1451+
button_data = [self.STANDARD_12, self.COMPACT_12]
1452+
else:
1453+
button_data = [self.STANDARD_24, self.COMPACT_24]
14491454

1450-
selected_menu_num = seed_screens.SeedTranscribeSeedQRFormatScreen(
1455+
selected_menu_num = self.run_screen(
1456+
seed_screens.SeedTranscribeSeedQRFormatScreen,
14511457
title=_("SeedQR Format"),
14521458
button_data=button_data,
1453-
).display()
1459+
)
14541460

14551461
if selected_menu_num == RET_CODE__BACK_BUTTON:
14561462
return Destination(BackStackView)
14571463

1458-
if button_data[selected_menu_num] == STANDARD:
1464+
if button_data[selected_menu_num] in [self.STANDARD_12, self.STANDARD_24]:
14591465
seedqr_format = QRType.SEED__SEEDQR
14601466
else:
14611467
seedqr_format = QRType.SEED__COMPACTSEEDQR
@@ -1496,10 +1502,11 @@ def run(self):
14961502
# Forward straight to transcribing the SeedQR
14971503
return destination
14981504

1499-
selected_menu_num = DireWarningScreen(
1505+
selected_menu_num = self.run_screen(
1506+
DireWarningScreen,
15001507
status_headline=_("SeedQR is your private key!"),
15011508
text=_("Never photograph or scan it into a device that connects to the internet."),
1502-
).display()
1509+
)
15031510

15041511
if selected_menu_num == RET_CODE__BACK_BUTTON:
15051512
return Destination(BackStackView)
@@ -1529,10 +1536,11 @@ def run(self):
15291536

15301537
data = e.next_part()
15311538

1532-
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
1539+
ret = self.run_screen(
1540+
seed_screens.SeedTranscribeSeedQRWholeQRScreen,
15331541
qr_data=data,
15341542
num_modules=self.num_modules,
1535-
).display()
1543+
)
15361544

15371545
if ret == RET_CODE__BACK_BUTTON:
15381546
return Destination(BackStackView)
@@ -1607,10 +1615,11 @@ def __init__(self, seed_num: int):
16071615
def run(self):
16081616
button_data = [self.SCAN, self.DONE]
16091617

1610-
selected_menu_option = seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen(
1618+
selected_menu_option = self.run_screen(
1619+
seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen,
16111620
title=_("Confirm SeedQR?"),
16121621
button_data=button_data,
1613-
).display()
1622+
)
16141623

16151624
if selected_menu_option == RET_CODE__BACK_BUTTON:
16161625
return Destination(BackStackView)
@@ -1625,61 +1634,100 @@ def run(self):
16251634

16261635
class SeedTranscribeSeedQRConfirmScanView(View):
16271636
def __init__(self, seed_num: int):
1637+
from seedsigner.models.decode_qr import DecodeQR
16281638
super().__init__()
16291639
self.seed_num = seed_num
16301640
self.seed = self.controller.get_seed(seed_num)
1641+
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
1642+
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
16311643

16321644
def run(self):
16331645
from seedsigner.gui.screens.scan_screens import ScanScreen
1634-
from seedsigner.models.decode_qr import DecodeQR
16351646

16361647
# Run the live preview and QR code capture process
16371648
# TODO: Does this belong in its own BaseThread?
1638-
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
1639-
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
1640-
ScanScreen(
1649+
self.run_screen(
1650+
ScanScreen,
16411651
decoder=self.decoder,
16421652
instructions_text=_("Scan your SeedQR")
1643-
).display()
1653+
)
16441654

16451655
if self.decoder.is_complete:
16461656
if self.decoder.is_seed:
16471657
seed_mnemonic = self.decoder.get_seed_phrase()
16481658
# Found a valid mnemonic seed! But does it match?
16491659
if seed_mnemonic != self.seed.mnemonic_list:
1650-
DireWarningScreen(
1651-
title=_("Confirm SeedQR"),
1652-
status_headline=_("Error!"),
1653-
text=_("Your transcribed SeedQR does not match your original seed!"),
1654-
show_back_button=False,
1655-
button_data=[_("Review SeedQR")],
1656-
).display()
1657-
1658-
return Destination(BackStackView, skip_current_view=True)
1659-
1660+
return Destination(SeedTranscribeSeedQRConfirmWrongSeedView, skip_current_view=True)
16601661
else:
1661-
from seedsigner.gui.screens.screen import LargeIconStatusScreen
1662-
LargeIconStatusScreen(
1663-
title=_("Confirm SeedQR"),
1664-
status_headline=_("Success!"),
1665-
text=_("Your transcribed SeedQR successfully scanned and yielded the same seed."),
1666-
show_back_button=False,
1667-
button_data=[_("OK")],
1668-
).display()
1662+
return Destination(SeedTranscribeSeedQRConfirmSuccessView, view_args={"seed_num": self.seed_num})
16691663

1670-
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
1664+
else:
1665+
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
1666+
return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True)
16711667

1672-
else:
1673-
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
1674-
DireWarningScreen(
1675-
title=_("Confirm SeedQR"),
1676-
status_headline=_("Error!"),
1677-
text=_("Your transcribed SeedQR could not be read!"),
1678-
show_back_button=False,
1679-
button_data=[_("Review SeedQR")],
1680-
).display()
1681-
1682-
return Destination(BackStackView, skip_current_view=True)
1668+
1669+
1670+
class SeedTranscribeSeedQRConfirmWrongSeedView(View):
1671+
"""
1672+
A valid SeedQR was scanned but it did NOT match the one we just transcribed!
1673+
"""
1674+
def run(self):
1675+
self.run_screen(
1676+
DireWarningScreen,
1677+
title=_("Confirm SeedQR"),
1678+
status_headline=_("Error!"),
1679+
text=_("Your transcribed SeedQR does not match your original seed!"),
1680+
show_back_button=False,
1681+
button_data=[ButtonOption("Review SeedQR")],
1682+
)
1683+
1684+
# Skip BACK to the zoomed in transcription view
1685+
return Destination(BackStackView, skip_current_view=True)
1686+
1687+
1688+
1689+
class SeedTranscribeSeedQRConfirmInvalidQRView(View):
1690+
"""
1691+
A QR code was scanned but it was not a SeedQR and certainly not the SeedQR we just
1692+
transcribed!
1693+
"""
1694+
def run(self):
1695+
# TODO: A better error message would be something like: "The QR code you scanned does not contain a valid SeedQR."
1696+
self.run_screen(
1697+
DireWarningScreen,
1698+
title=_("Confirm SeedQR"),
1699+
status_headline=_("Error!"),
1700+
text=_("Your transcribed SeedQR could not be read!"),
1701+
show_back_button=False,
1702+
button_data=[ButtonOption("Review SeedQR")],
1703+
)
1704+
1705+
# Skip BACK to the zoomed in transcription view
1706+
return Destination(BackStackView, skip_current_view=True)
1707+
1708+
1709+
1710+
class SeedTranscribeSeedQRConfirmSuccessView(View):
1711+
"""
1712+
The SeedQR we just scanned matched the one we just transcribed.
1713+
"""
1714+
def __init__(self, seed_num: int):
1715+
super().__init__()
1716+
self.seed_num = seed_num
1717+
1718+
1719+
def run(self):
1720+
from seedsigner.gui.screens.screen import LargeIconStatusScreen
1721+
self.run_screen(
1722+
LargeIconStatusScreen,
1723+
title=_("Confirm SeedQR"),
1724+
status_headline=_("Success!"),
1725+
text=_("Your transcribed SeedQR successfully scanned and yielded the same seed."),
1726+
show_back_button=False,
1727+
button_data=[ButtonOption("OK")],
1728+
)
1729+
1730+
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
16831731

16841732

16851733

tests/base.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.modules['seedsigner.hardware.camera'] = MagicMock()
1414

1515
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
16-
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
16+
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON, ButtonOption
1717
from seedsigner.hardware.microsd import MicroSD
1818
from seedsigner.models.settings import Settings
1919
from seedsigner.views.view import Destination, MainMenuView, UnhandledExceptionView, View
@@ -140,6 +140,12 @@ def __post_init__(self):
140140

141141

142142

143+
class FlowTestInvalidButtonDataInstanceTypeException(FlowBasedTestException):
144+
""" The button_data contained an item that was not a ButtonOption instance """
145+
pass
146+
147+
148+
143149
class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException):
144150
""" The FlowStep's button_data_selection value was not found in the View's button_data """
145151
pass
@@ -250,6 +256,12 @@ def run_screen(view: View, *args, **kwargs):
250256
"""
251257
cur_flow_step = sequence[0]
252258

259+
if "button_data" in kwargs:
260+
# Verify that they are all proper ButtonOption instances
261+
for button_option in kwargs.get("button_data"):
262+
if not isinstance(button_option, ButtonOption):
263+
raise FlowTestInvalidButtonDataInstanceTypeException(f"button_data must be a list of ButtonOption instances, not {type(button_option)}: {button_option}")
264+
253265
if cur_flow_step.button_data_selection:
254266
# We're mocking out the View.run_screen() method, so we'll get all of the
255267
# input args that are normally passed into the Screen.run() method,

tests/screenshot_generator/generator.py

+3
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ def PSBTOpReturnView_raw_hex_data_cb_before():
290290
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"),
291291

292292
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
293+
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
294+
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
295+
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmSuccessView, dict(seed_num=0)),
293296

294297
# Screenshot can't render live preview screens
295298
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),

tests/test_flows.py

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import pytest
22

33
# Must import test base before the Controller
4-
from base import FlowTest, FlowStep, FlowTestMissingRedirectException, FlowTestUnexpectedRedirectException, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException
4+
from base import FlowTest, FlowStep, FlowTestMissingRedirectException, FlowTestUnexpectedRedirectException, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException, FlowTestInvalidButtonDataInstanceTypeException
55

66
from seedsigner.controller import Controller
7-
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
7+
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON, ButtonListScreen, ButtonOption
88
from seedsigner.models.seed import Seed
99
from seedsigner.views import scan_views
1010
from seedsigner.views.psbt_views import PSBTSelectSeedView
1111
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedsMenuView
12-
from seedsigner.views.view import MainMenuView, PowerOptionsView, UnhandledExceptionView
12+
from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, View
1313
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
1414

1515

@@ -156,4 +156,39 @@ def test_raise_exception_via_screen_return_value(self):
156156
FlowStep(MainMenuView, screen_return_value=Exception("Test exception")),
157157
FlowStep(UnhandledExceptionView),
158158
])
159+
160+
161+
def test_raise_exception_on_bad_button_data_type(self):
162+
"""
163+
Ensure that the FlowTest raises an exception if a Screen's button_data has
164+
non-ButtonOption entries.
165+
"""
166+
class MyBadButtonDataTestView(View):
167+
def run(self):
168+
self.run_screen(
169+
ButtonListScreen,
170+
button_data=[ButtonOption("this is fine"), "this is not"]
171+
)
172+
173+
class MyGoodButtonDataTestView(View):
174+
def run(self):
175+
self.run_screen(
176+
ButtonListScreen,
177+
button_data=[ButtonOption("this is fine"), ButtonOption("this is also fine")]
178+
)
179+
return Destination(MainMenuView)
180+
181+
182+
# Should catch the bad button_data
183+
with pytest.raises(FlowTestInvalidButtonDataInstanceTypeException):
184+
self.run_sequence([
185+
FlowStep(MyBadButtonDataTestView),
186+
FlowStep(MainMenuView), # Need a next Destination to force the first step to run
187+
])
188+
189+
# But if it's all ButtonOption instances, it should be fine
190+
self.run_sequence([
191+
FlowStep(MyGoodButtonDataTestView),
192+
FlowStep(MainMenuView), # Need a next Destination to force the first step to run
193+
])
159194

0 commit comments

Comments
 (0)