mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #339 from kdmukai/test_flows
[Enhancement] Enable flow-based test scenarios
This commit is contained in:
@@ -177,6 +177,7 @@ cd seedsigner
|
||||
```bash
|
||||
# Takes 1hr 45min on a Pi Zero 1.3
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-raspi.txt
|
||||
```
|
||||
|
||||
#### `pyzbar`
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
picamera==1.13
|
||||
RPi.GPIO==0.7.0
|
||||
spidev==3.5
|
||||
@@ -1,10 +1,7 @@
|
||||
embit==0.7.0
|
||||
numpy==1.21.1
|
||||
picamera==1.13
|
||||
Pillow==8.2.0
|
||||
-e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
|
||||
qrcode==7.3.1
|
||||
RPi.GPIO==0.7.0
|
||||
six==1.16.0
|
||||
spidev==3.5
|
||||
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
|
||||
|
||||
@@ -32,6 +32,23 @@ class BackStack(List[Destination]):
|
||||
|
||||
|
||||
|
||||
class StopFlowBasedTest(Exception):
|
||||
"""
|
||||
This is a special exception that is only raised by the test suite to stop the
|
||||
Controller's main loop. It should not be raised by any other code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FlowBasedTestException(Exception):
|
||||
"""
|
||||
This is a special exception that is only raised by the test suite.
|
||||
It should not be raised by any other code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Controller(Singleton):
|
||||
"""
|
||||
The Controller is a globally available singleton that maintains SeedSigner state.
|
||||
@@ -185,7 +202,13 @@ class Controller(Singleton):
|
||||
self.back_stack = BackStack()
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
def start(self, initial_destination: Destination = None) -> None:
|
||||
"""
|
||||
The main loop of the application.
|
||||
|
||||
* initial_destination: The first View to run. If None, the MainMenuView is
|
||||
used. Only used by the test suite.
|
||||
"""
|
||||
from .views import MainMenuView, BackStackView
|
||||
from .views.screensaver import OpeningSplashScreen
|
||||
|
||||
@@ -218,7 +241,10 @@ class Controller(Singleton):
|
||||
View_cls(**init_args).run()
|
||||
"""
|
||||
try:
|
||||
next_destination = Destination(MainMenuView)
|
||||
if initial_destination:
|
||||
next_destination = initial_destination
|
||||
else:
|
||||
next_destination = Destination(MainMenuView)
|
||||
while True:
|
||||
# Destination(None) is a special case; render the Home screen
|
||||
if next_destination.View_cls is None:
|
||||
@@ -240,10 +266,24 @@ class Controller(Singleton):
|
||||
print(f"back_stack: {self.back_stack}")
|
||||
|
||||
try:
|
||||
# Instantiate the View class and run it
|
||||
print(f"Executing {next_destination}")
|
||||
next_destination = next_destination.run()
|
||||
|
||||
except StopFlowBasedTest:
|
||||
# This is a special exception that is only raised by the test suite
|
||||
# to stop the Controller loop and exit the test.
|
||||
return
|
||||
|
||||
except FlowBasedTestException as e:
|
||||
# This is a special exception that is only raised by the test suite.
|
||||
# Re-raise so the test suite can handle it.
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
# Display user-friendly error screen w/debugging info
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
next_destination = self.handle_exception(e)
|
||||
|
||||
if not next_destination:
|
||||
|
||||
@@ -217,7 +217,7 @@ class SettingsEntry:
|
||||
visibility: str = SettingsConstants.VISIBILITY__GENERAL
|
||||
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
|
||||
help_text: str = None
|
||||
selection_options: List[str] = None
|
||||
selection_options: list[tuple[str | int], str] = None
|
||||
default_value: Any = None
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
@@ -11,9 +11,8 @@ from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.gui.screens import psbt_screens
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen,
|
||||
LoadingScreenThread, QRDisplayScreen, WarningScreen)
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen, QRDisplayScreen)
|
||||
|
||||
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
@@ -22,6 +21,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PSBTSelectSeedView(View):
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
|
||||
def run(self):
|
||||
# Note: we can't just autoroute to the PSBT Overview because we might have a
|
||||
# multisig where we want to sign with more than one key on this device.
|
||||
@@ -31,32 +35,29 @@ class PSBTSelectSeedView(View):
|
||||
|
||||
seeds = self.controller.storage.seeds
|
||||
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# Doesn't look like this seed can sign the current PSBT
|
||||
button_str += " (?)"
|
||||
|
||||
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
self.button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
|
||||
button_data.append(SCAN_SEED)
|
||||
button_data.append(TYPE_12WORD)
|
||||
button_data.append(TYPE_24WORD)
|
||||
self.button_data.append(self.SCAN_SEED)
|
||||
self.button_data.append(self.TYPE_12WORD)
|
||||
self.button_data.append(self.TYPE_24WORD)
|
||||
|
||||
if self.controller.psbt_seed:
|
||||
if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# skip the seed prompt if a seed was previous selected and has matching input fingerprint
|
||||
return Destination(PSBTOverviewView)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Select Signer",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
button_data=self.button_data
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -69,13 +70,13 @@ class PSBTSelectSeedView(View):
|
||||
# The remaining flows are a sub-flow; resume PSBT flow once the seed is loaded.
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
|
||||
if button_data[selected_menu_num] == SCAN_SEED:
|
||||
if self.button_data[selected_menu_num] == self.SCAN_SEED:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == TYPE_12WORD:
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
@@ -92,8 +93,10 @@ class PSBTOverviewView(View):
|
||||
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
|
||||
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
|
||||
# we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
||||
self.loading_screen.start()
|
||||
|
||||
try:
|
||||
self.controller.psbt_parser = PSBTParser(
|
||||
self.controller.psbt,
|
||||
@@ -128,23 +131,22 @@ class PSBTOverviewView(View):
|
||||
else:
|
||||
num_self_transfer_outputs += 1
|
||||
|
||||
screen = psbt_screens.PSBTOverviewScreen(
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
self.loading_screen.stop()
|
||||
|
||||
# Run the overview screen
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTOverviewScreen,
|
||||
spend_amount=psbt_parser.spend_amount,
|
||||
change_amount=psbt_parser.change_amount,
|
||||
fee_amount=psbt_parser.fee_amount,
|
||||
num_inputs=psbt_parser.num_inputs,
|
||||
num_self_transfer_outputs=num_self_transfer_outputs,
|
||||
num_change_outputs=num_change_outputs,
|
||||
destination_addresses=psbt_parser.destination_addresses,
|
||||
destination_addresses=psbt_parser.destination_addresses
|
||||
)
|
||||
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
self.loading_screen.stop()
|
||||
|
||||
# Run the overview screen
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
self.controller.psbt_seed = None
|
||||
return Destination(BackStackView)
|
||||
@@ -214,14 +216,15 @@ class PSBTMathView(View):
|
||||
# Should not be able to get here
|
||||
return Destination(MainMenuView)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTMathScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTMathScreen,
|
||||
input_amount=psbt_parser.input_amount,
|
||||
num_inputs=psbt_parser.num_inputs,
|
||||
spend_amount=psbt_parser.spend_amount,
|
||||
num_recipients=psbt_parser.num_destinations,
|
||||
fee_amount=psbt_parser.fee_amount,
|
||||
change_amount=psbt_parser.change_amount,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -238,7 +241,10 @@ class PSBTAddressDetailsView(View):
|
||||
"""
|
||||
Shows the recipient's address and amount they will receive
|
||||
"""
|
||||
def __init__(self, address_num, is_change=False):
|
||||
NEXT = "Next"
|
||||
button_data = []
|
||||
|
||||
def __init__(self, address_num):
|
||||
super().__init__()
|
||||
self.address_num = address_num
|
||||
|
||||
@@ -254,23 +260,27 @@ class PSBTAddressDetailsView(View):
|
||||
if psbt_parser.num_destinations > 1:
|
||||
title += f" (#{self.address_num + 1})"
|
||||
|
||||
button_data = []
|
||||
if self.address_num < psbt_parser.num_destinations - 1:
|
||||
button_data.append("Next Recipient")
|
||||
self.NEXT = "Next Recipient"
|
||||
else:
|
||||
button_data.append("Next")
|
||||
self.NEXT = "Next"
|
||||
self.button_data.append(self.NEXT)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTAddressDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTAddressDetailsScreen,
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
button_data=self.button_data,
|
||||
address=psbt_parser.destination_addresses[self.address_num],
|
||||
amount=psbt_parser.destination_amounts[self.address_num],
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
if self.button_data[selected_menu_num] == self.NEXT:
|
||||
if self.address_num < len(psbt_parser.destination_addresses) - 1:
|
||||
# Show the next receive addr
|
||||
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1, "is_change": False})
|
||||
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1})
|
||||
|
||||
elif psbt_parser.change_amount > 0:
|
||||
# Move on to display change
|
||||
@@ -280,14 +290,13 @@ class PSBTAddressDetailsView(View):
|
||||
# There's no change output to verify. Move on to sign the PSBT.
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
|
||||
|
||||
class PSBTChangeDetailsView(View):
|
||||
"""
|
||||
"""
|
||||
NEXT = "Next"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
button_data = [NEXT]
|
||||
|
||||
def __init__(self, change_address_num):
|
||||
super().__init__()
|
||||
self.change_address_num = change_address_num
|
||||
@@ -327,14 +336,12 @@ class PSBTChangeDetailsView(View):
|
||||
is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1
|
||||
derivation_path_addr_index = int(derivation_path.split("/")[-1])
|
||||
|
||||
NEXT = "Next"
|
||||
|
||||
if is_change_derivation_path:
|
||||
title = "Your Change"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
else:
|
||||
title = "Self-Transfer"
|
||||
VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
# if psbt_parser.num_change_outputs > 1:
|
||||
# title += f" (#{self.change_address_num + 1})"
|
||||
|
||||
@@ -343,11 +350,11 @@ class PSBTChangeDetailsView(View):
|
||||
# if the known-good multisig descriptor is already onboard:
|
||||
if self.controller.multisig_wallet_descriptor:
|
||||
is_change_addr_verified = psbt_parser.verify_multisig_output(self.controller.multisig_wallet_descriptor, change_num=self.change_address_num)
|
||||
button_data = [NEXT]
|
||||
self.button_data = [self.NEXT]
|
||||
|
||||
else:
|
||||
# Have the Screen offer to load in the multisig descriptor.
|
||||
button_data = [VERIFY_MULTISIG, NEXT]
|
||||
self.button_data = [self.VERIFY_MULTISIG, self.NEXT]
|
||||
|
||||
else:
|
||||
# Single sig
|
||||
@@ -356,6 +363,7 @@ class PSBTChangeDetailsView(View):
|
||||
loading_screen_text = "Verifying Change..."
|
||||
else:
|
||||
loading_screen_text = "Verifying Self-Transfer..."
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
loading_screen = LoadingScreenThread(text=loading_screen_text)
|
||||
loading_screen.start()
|
||||
|
||||
@@ -389,7 +397,7 @@ class PSBTChangeDetailsView(View):
|
||||
|
||||
if change_data["address"] == calc_address:
|
||||
is_change_addr_verified = True
|
||||
button_data = [NEXT]
|
||||
self.button_data = [self.NEXT]
|
||||
|
||||
finally:
|
||||
loading_screen.stop()
|
||||
@@ -397,9 +405,10 @@ class PSBTChangeDetailsView(View):
|
||||
if is_change_addr_verified == False and (not psbt_parser.is_multisig or self.controller.multisig_wallet_descriptor is not None):
|
||||
return Destination(PSBTAddressVerificationFailedView, view_args=dict(is_change=is_change_derivation_path, is_multisig=psbt_parser.is_multisig), clear_history=True)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTChangeDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTChangeDetailsScreen,
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
button_data=self.button_data,
|
||||
address=change_data.get("address"),
|
||||
amount=change_data.get("amount"),
|
||||
is_multisig=psbt_parser.is_multisig,
|
||||
@@ -408,19 +417,19 @@ class PSBTChangeDetailsView(View):
|
||||
is_change_derivation_path=is_change_derivation_path,
|
||||
derivation_path_addr_index=derivation_path_addr_index,
|
||||
is_change_addr_verified=is_change_addr_verified,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == NEXT:
|
||||
elif self.button_data[selected_menu_num] == self.NEXT:
|
||||
if self.change_address_num < psbt_parser.num_change_outputs - 1:
|
||||
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
|
||||
else:
|
||||
# There's no more change to verify. Move on to sign the PSBT.
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == VERIFY_MULTISIG:
|
||||
elif self.button_data[selected_menu_num] == self.VERIFY_MULTISIG:
|
||||
from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
return Destination(LoadMultisigWalletDescriptorView)
|
||||
@@ -459,6 +468,9 @@ class PSBTAddressVerificationFailedView(View):
|
||||
class PSBTFinalizeView(View):
|
||||
"""
|
||||
"""
|
||||
APPROVE_PSBT = "Approve PSBT"
|
||||
button_data = [APPROVE_PSBT]
|
||||
|
||||
def run(self):
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
psbt: PSBT = self.controller.psbt
|
||||
@@ -466,12 +478,13 @@ class PSBTFinalizeView(View):
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
return Destination(MainMenuView)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTFinalizeScreen,
|
||||
button_data=self.button_data
|
||||
)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTFinalizeScreen(
|
||||
button_data=["Approve PSBT"]
|
||||
).display()
|
||||
|
||||
if selected_menu_num == 0:
|
||||
if self.button_data[selected_menu_num] == self.APPROVE_PSBT:
|
||||
# Sign PSBT
|
||||
sig_cnt = PSBTParser.sig_count(psbt)
|
||||
psbt.sign_with(psbt_parser.root)
|
||||
@@ -500,7 +513,7 @@ class PSBTSignedQRDisplayView(View):
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
||||
)
|
||||
QRDisplayScreen(qr_encoder=qr_encoder).display()
|
||||
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
|
||||
|
||||
# We're done with this PSBT. Route back to MainMenuView which always
|
||||
# clears all ephemeral data (except in-memory seeds).
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens import scan_screens
|
||||
from seedsigner.models import DecodeQR, Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
@@ -12,19 +13,24 @@ from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Dest
|
||||
|
||||
|
||||
class ScanView(View):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set up the QR decoder here so we can inject data into it in the test suite's
|
||||
# `before_run`.
|
||||
self.wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
self.decoder = DecodeQR(wordlist_language_code=self.wordlist_language_code)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.scan_screens import ScanScreen
|
||||
|
||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
|
||||
|
||||
# Start the live preview and background QR reading
|
||||
ScanScreen(decoder=self.decoder).display()
|
||||
self.run_screen(scan_screens.ScanScreen, decoder=self.decoder)
|
||||
|
||||
# Handle the results
|
||||
if self.decoder.is_complete:
|
||||
if self.decoder.is_seed:
|
||||
seed_mnemonic = self.decoder.get_seed_phrase()
|
||||
|
||||
if not seed_mnemonic:
|
||||
# seed is not valid, Exit if not valid with message
|
||||
raise Exception("Not yet implemented!")
|
||||
@@ -33,7 +39,7 @@ class ScanView(View):
|
||||
# pending (might set a passphrase, SeedXOR, etc) until finalized.
|
||||
from .seed_views import SeedFinalizeView
|
||||
self.controller.storage.set_pending_seed(
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code)
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code)
|
||||
)
|
||||
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED:
|
||||
from seedsigner.views.seed_views import SeedAddPassphraseView
|
||||
|
||||
+160
-140
@@ -13,7 +13,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustom
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
WarningScreen, DireWarningScreen, seed_screens)
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
@@ -31,6 +31,8 @@ from .view import NotYetImplementedView, View, Destination, BackStackView, MainM
|
||||
|
||||
|
||||
class SeedsMenuView(View):
|
||||
LOAD = "Load a seed"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seeds = []
|
||||
@@ -50,11 +52,12 @@ class SeedsMenuView(View):
|
||||
button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
button_data.append("Load a seed")
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="In-Memory Seeds",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if len(self.seeds) > 0 and selected_menu_num < len(self.seeds):
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": selected_menu_num})
|
||||
@@ -71,40 +74,41 @@ class SeedsMenuView(View):
|
||||
Loading seeds, passphrases, etc
|
||||
****************************************************************************"""
|
||||
class LoadSeedView(View):
|
||||
def run(self):
|
||||
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
||||
button_data=[
|
||||
SEED_QR,
|
||||
TYPE_12WORD,
|
||||
TYPE_24WORD,
|
||||
CREATE,
|
||||
]
|
||||
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
def run(self):
|
||||
button_data = [
|
||||
self.SEED_QR,
|
||||
self.TYPE_12WORD,
|
||||
self.TYPE_24WORD,
|
||||
self.CREATE,
|
||||
]
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Load A Seed",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == SEED_QR:
|
||||
if button_data[selected_menu_num] == self.SEED_QR:
|
||||
from .scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == TYPE_12WORD:
|
||||
elif button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == TYPE_24WORD:
|
||||
elif button_data[selected_menu_num] == self.TYPE_24WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == CREATE:
|
||||
elif button_data[selected_menu_num] == self.CREATE:
|
||||
from .tools_views import ToolsMenuView
|
||||
return Destination(ToolsMenuView)
|
||||
|
||||
@@ -119,22 +123,16 @@ class SeedMnemonicEntryView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedMnemonicEntryScreen(
|
||||
ret = self.run_screen(
|
||||
seed_screens.SeedMnemonicEntryScreen,
|
||||
title=f"Seed Word #{self.cur_word_index + 1}", # Human-readable 1-indexing!
|
||||
initial_letters=list(self.cur_word) if self.cur_word else ["a"],
|
||||
wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)),
|
||||
).display()
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
if self.cur_word_index > 0:
|
||||
return Destination(BackStackView)
|
||||
# return Destination(
|
||||
# SeedMnemonicEntryView,
|
||||
# view_args={
|
||||
# "cur_word_index": self.cur_word_index - 1,
|
||||
# "is_calc_final_word": self.is_calc_final_word
|
||||
# }
|
||||
# )
|
||||
else:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
@@ -174,34 +172,38 @@ class SeedMnemonicEntryView(View):
|
||||
|
||||
|
||||
class SeedMnemonicInvalidView(View):
|
||||
EDIT = "Review & Edit"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mnemonic: List[str] = self.controller.storage.pending_mnemonic
|
||||
|
||||
|
||||
def run(self):
|
||||
EDIT = "Review & Edit"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [EDIT, DISCARD]
|
||||
|
||||
selected_menu_num = WarningScreen(
|
||||
button_data = [self.EDIT, self.DISCARD]
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Invalid Mnemonic!",
|
||||
status_headline=None,
|
||||
text=f"Checksum failure; not a valid seed phrase.",
|
||||
show_back_button=False,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == EDIT:
|
||||
if button_data[selected_menu_num] == self.EDIT:
|
||||
return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0})
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
|
||||
|
||||
|
||||
class SeedFinalizeView(View):
|
||||
FINALIZE = "Done"
|
||||
PASSPHRASE = "BIP-39 Passphrase"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seed = self.controller.storage.get_pending_seed()
|
||||
@@ -209,25 +211,21 @@ class SeedFinalizeView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
FINALIZE = "Done"
|
||||
PASSPHRASE = "BIP-39 Passphrase"
|
||||
button_data = []
|
||||
|
||||
button_data.append(FINALIZE)
|
||||
|
||||
button_data = [self.FINALIZE]
|
||||
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
|
||||
button_data.append(PASSPHRASE)
|
||||
button_data.append(self.PASSPHRASE)
|
||||
|
||||
selected_menu_num = seed_screens.SeedFinalizeScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedFinalizeScreen,
|
||||
fingerprint=self.fingerprint,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == FINALIZE:
|
||||
if button_data[selected_menu_num] == self.FINALIZE:
|
||||
seed_num = self.controller.storage.finalize_pending_seed()
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
||||
|
||||
elif button_data[selected_menu_num] == PASSPHRASE:
|
||||
elif button_data[selected_menu_num] == self.PASSPHRASE:
|
||||
return Destination(SeedAddPassphraseView)
|
||||
|
||||
|
||||
@@ -239,7 +237,7 @@ class SeedAddPassphraseView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedAddPassphraseScreen(passphrase=self.seed.passphrase).display()
|
||||
ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -257,16 +255,15 @@ class SeedReviewPassphraseView(View):
|
||||
"""
|
||||
Display the completed passphrase back to the user.
|
||||
"""
|
||||
EDIT = "Edit passphrase"
|
||||
DONE = "Done"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seed = self.controller.storage.get_pending_seed()
|
||||
|
||||
|
||||
def run(self):
|
||||
EDIT = "Edit passphrase"
|
||||
DONE = "Done"
|
||||
button_data = [EDIT, DONE]
|
||||
|
||||
# Get the before/after fingerprints
|
||||
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
|
||||
passphrase = self.seed.passphrase
|
||||
@@ -275,26 +272,32 @@ class SeedReviewPassphraseView(View):
|
||||
fingerprint_without = self.seed.get_fingerprint(network=network)
|
||||
self.seed.set_passphrase(passphrase)
|
||||
|
||||
# Because we have ane explicit "Edit" button, we disable "BACK" to keep the
|
||||
button_data = [self.EDIT, self.DONE]
|
||||
|
||||
# Because we have an explicit "Edit" button, we disable "BACK" to keep the
|
||||
# routing options sane.
|
||||
selected_menu_num = seed_screens.SeedReviewPassphraseScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedReviewPassphraseScreen,
|
||||
fingerprint_without=fingerprint_without,
|
||||
fingerprint_with=fingerprint_with,
|
||||
passphrase=self.seed.passphrase,
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == EDIT:
|
||||
if button_data[selected_menu_num] == self.EDIT:
|
||||
return Destination(SeedAddPassphraseView)
|
||||
|
||||
elif button_data[selected_menu_num] == DONE:
|
||||
elif button_data[selected_menu_num] == self.DONE:
|
||||
seed_num = self.controller.storage.finalize_pending_seed()
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
||||
|
||||
|
||||
|
||||
class SeedDiscardView(View):
|
||||
KEEP = "Keep Seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
|
||||
def __init__(self, seed_num: int = None):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -305,27 +308,26 @@ class SeedDiscardView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
KEEP = "Keep Seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [KEEP, DISCARD]
|
||||
button_data = [self.KEEP, self.DISCARD]
|
||||
|
||||
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Discard Seed?",
|
||||
status_headline=None,
|
||||
text=f"Wipe seed {fingerprint} from the device?",
|
||||
show_back_button=False,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == KEEP:
|
||||
if button_data[selected_menu_num] == self.KEEP:
|
||||
# Use skip_current_view=True to prevent BACK from landing on this warning screen
|
||||
if self.seed_num is not None:
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, skip_current_view=True)
|
||||
else:
|
||||
return Destination(SeedFinalizeView, skip_current_view=True)
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
if self.seed_num is not None:
|
||||
self.controller.discard_seed(self.seed_num)
|
||||
else:
|
||||
@@ -338,6 +340,15 @@ class SeedDiscardView(View):
|
||||
Views for actions on individual seeds:
|
||||
****************************************************************************"""
|
||||
class SeedOptionsView(View):
|
||||
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
||||
VERIFY_ADDRESS = "Verify Addr"
|
||||
EXPORT_XPUB = "Export Xpub"
|
||||
EXPLORER = "Address Explorer"
|
||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
||||
DISCARD = ("Discard Seed", None, None, "red")
|
||||
|
||||
|
||||
def __init__(self, seed_num: int):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -347,90 +358,87 @@ class SeedOptionsView(View):
|
||||
def run(self):
|
||||
from seedsigner.views.psbt_views import PSBTOverviewView
|
||||
|
||||
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
||||
VERIFY_ADDRESS = "Verify Addr"
|
||||
EXPORT_XPUB = "Export Xpub"
|
||||
EXPLORER = "Address Explorer"
|
||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
||||
DISCARD = ("Discard Seed", None, None, "red")
|
||||
|
||||
button_data = []
|
||||
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
# Jump straight back into the address explorer script type selection flow
|
||||
# But do ont cancel the `resume_main_flow` as we'll still need that after
|
||||
# derivation path is specified.
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
||||
|
||||
if self.controller.unverified_address:
|
||||
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
||||
# Jump straight back into the single sig addr verification flow
|
||||
self.controller.resume_main_flow = None
|
||||
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num), skip_current_view=True)
|
||||
|
||||
addr = self.controller.unverified_address["address"][:7]
|
||||
VERIFY_ADDRESS += f" {addr}"
|
||||
button_data.append(VERIFY_ADDRESS)
|
||||
|
||||
if self.controller.psbt:
|
||||
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
||||
# Re-route us directly back to the start of the PSBT flow
|
||||
self.controller.resume_main_flow = None
|
||||
self.controller.psbt_seed = self.seed
|
||||
return Destination(PSBTOverviewView, skip_current_view=True)
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
# Jump straight back into the address explorer script type selection flow
|
||||
# But don't cancel the `resume_main_flow` as we'll still need that after
|
||||
# derivation path is specified.
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
||||
|
||||
button_data.append(SCAN_PSBT)
|
||||
if self.controller.psbt:
|
||||
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
||||
# Re-route us directly back to the start of the PSBT flow
|
||||
self.controller.resume_main_flow = None
|
||||
self.controller.psbt_seed = self.seed
|
||||
return Destination(PSBTOverviewView, skip_current_view=True)
|
||||
|
||||
button_data = []
|
||||
|
||||
if self.controller.unverified_address:
|
||||
addr = self.controller.unverified_address["address"][:7]
|
||||
self.VERIFY_ADDRESS += f" {addr}"
|
||||
button_data.append(self.VERIFY_ADDRESS)
|
||||
|
||||
button_data.append(self.SCAN_PSBT)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(EXPORT_XPUB)
|
||||
button_data.append(self.EXPORT_XPUB)
|
||||
|
||||
button_data.append(EXPLORER)
|
||||
button_data.append(BACKUP)
|
||||
button_data.append(self.EXPLORER)
|
||||
button_data.append(self.BACKUP)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(BIP85_CHILD_SEED)
|
||||
button_data.append(self.BIP85_CHILD_SEED)
|
||||
|
||||
button_data.append(DISCARD)
|
||||
|
||||
selected_menu_num = seed_screens.SeedOptionsScreen(
|
||||
button_data.append(self.DISCARD)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedOptionsScreen,
|
||||
button_data=button_data,
|
||||
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
||||
has_passphrase=self.seed.passphrase is not None,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
# Force BACK to always return to the Main Menu
|
||||
return Destination(MainMenuView)
|
||||
|
||||
if button_data[selected_menu_num] == SCAN_PSBT:
|
||||
if button_data[selected_menu_num] == self.SCAN_PSBT:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
|
||||
elif button_data[selected_menu_num] == self.VERIFY_ADDRESS:
|
||||
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == EXPORT_XPUB:
|
||||
elif button_data[selected_menu_num] == self.EXPORT_XPUB:
|
||||
return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
elif button_data[selected_menu_num] == self.EXPLORER:
|
||||
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
|
||||
|
||||
elif button_data[selected_menu_num] == BACKUP:
|
||||
elif button_data[selected_menu_num] == self.BACKUP:
|
||||
return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
|
||||
elif button_data[selected_menu_num] == self.BIP85_CHILD_SEED:
|
||||
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
|
||||
|
||||
class SeedBackupView(View):
|
||||
VIEW_WORDS = "View Seed Words"
|
||||
EXPORT_SEEDQR = "Export as SeedQR"
|
||||
|
||||
def __init__(self, seed_num):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -438,23 +446,22 @@ class SeedBackupView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
VIEW_WORDS = "View Seed Words"
|
||||
EXPORT_SEEDQR = "Export as SeedQR"
|
||||
button_data = [VIEW_WORDS, EXPORT_SEEDQR]
|
||||
button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Backup Seed",
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == VIEW_WORDS:
|
||||
elif button_data[selected_menu_num] == self.VIEW_WORDS:
|
||||
return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
elif button_data[selected_menu_num] == EXPORT_SEEDQR:
|
||||
elif button_data[selected_menu_num] == self.EXPORT_SEEDQR:
|
||||
return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
|
||||
@@ -463,6 +470,9 @@ class SeedBackupView(View):
|
||||
Export Xpub flow
|
||||
****************************************************************************"""
|
||||
class SeedExportXpubSigTypeView(View):
|
||||
SINGLE_SIG = "Single Sig"
|
||||
MULTISIG = "Multisig"
|
||||
|
||||
def __init__(self, seed_num: int):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -473,22 +483,21 @@ class SeedExportXpubSigTypeView(View):
|
||||
# Nothing to select; skip this screen
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True)
|
||||
|
||||
SINGLE_SIG = "Single Sig"
|
||||
MULTISIG = "Multisig"
|
||||
button_data=[SINGLE_SIG, MULTISIG]
|
||||
button_data=[self.SINGLE_SIG, self.MULTISIG]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Export Xpub",
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == SINGLE_SIG:
|
||||
if button_data[selected_menu_num] == self.SINGLE_SIG:
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.SINGLE_SIG})
|
||||
|
||||
elif button_data[selected_menu_num] == MULTISIG:
|
||||
elif button_data[selected_menu_num] == self.MULTISIG:
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.MULTISIG})
|
||||
|
||||
|
||||
@@ -512,21 +521,22 @@ class SeedExportXpubScriptTypeView(View):
|
||||
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
|
||||
else:
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
||||
|
||||
button_data = []
|
||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||
button_data.append(script_type)
|
||||
|
||||
title = "Export Xpub"
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
title = "Address Explorer"
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
button_data = []
|
||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||
button_data.append(script_type)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
# If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped).
|
||||
@@ -558,10 +568,12 @@ class SeedExportXpubCustomDerivationView(View):
|
||||
self.script_type = script_type
|
||||
self.custom_derivation_path = "m/"
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedExportXpubCustomDerivationScreen(
|
||||
ret = self.run_screen(
|
||||
seed_screens.SeedExportXpubCustomDerivationScreen,
|
||||
initial_value=self.custom_derivation_path,
|
||||
).display()
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -606,11 +618,12 @@ class SeedExportXpubCoordinatorView(View):
|
||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
||||
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Export Xpub",
|
||||
is_button_text_centered=False,
|
||||
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
|
||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
|
||||
@@ -648,10 +661,11 @@ class SeedExportXpubWarningView(View):
|
||||
# Skip the WarningView entirely
|
||||
return destination
|
||||
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
status_headline="Privacy Leak!",
|
||||
text="""Xpub can be used to view all future transactions.""",
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# User clicked "I Understand"
|
||||
@@ -694,6 +708,7 @@ class SeedExportXpubDetailsView(View):
|
||||
|
||||
else:
|
||||
# The derivation calc takes a few moments. Run the loading screen while we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
||||
self.loading_screen.start()
|
||||
|
||||
@@ -716,12 +731,13 @@ class SeedExportXpubDetailsView(View):
|
||||
finally:
|
||||
self.loading_screen.stop()
|
||||
|
||||
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedExportXpubDetailsScreen,
|
||||
fingerprint=fingerprint,
|
||||
has_passphrase=self.seed.passphrase is not None,
|
||||
derivation_path=derivation_path,
|
||||
xpub=xpub_base58,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
return Destination(
|
||||
@@ -773,7 +789,10 @@ class SeedExportXpubQRDisplayView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
QRDisplayScreen(qr_encoder=self.qr_encoder).display()
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
qr_encoder=self.qr_encoder
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
@@ -803,9 +822,10 @@ class SeedWordsWarningView(View):
|
||||
# Forward straight to showing the words
|
||||
return destination
|
||||
|
||||
selected_menu_num = DireWarningScreen(
|
||||
text="""Never input your seed phrase into a device that connects to the internet.""",
|
||||
).display()
|
||||
selected_menu_num = self.run_screen(
|
||||
DireWarningScreen,
|
||||
text="""You must keep your seed words private & away from all online devices.""",
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# User clicked "I Understand"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.gui.components import SeedSignerCustomIconConstants
|
||||
|
||||
from .view import View, Destination, BackStackView, MainMenuView
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.models.settings import SettingsConstants, SettingsDefinition
|
||||
@@ -9,6 +8,9 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition
|
||||
|
||||
|
||||
class SettingsMenuView(View):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
|
||||
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
self.visibility = visibility
|
||||
@@ -19,9 +21,6 @@ class SettingsMenuView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
|
||||
settings_entries = SettingsDefinition.get_settings_entries(
|
||||
visibility=self.visibility
|
||||
)
|
||||
@@ -39,34 +38,34 @@ class SettingsMenuView(View):
|
||||
|
||||
# Set up the next nested level of menuing
|
||||
button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
button_data.append(IO_TEST)
|
||||
button_data.append(DONATE)
|
||||
button_data.append(self.IO_TEST)
|
||||
button_data.append(self.DONATE)
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||
title = "Advanced"
|
||||
|
||||
# So far there are no real Developer options; disabling for now
|
||||
# button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
# next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
|
||||
next = None
|
||||
# next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
|
||||
next_destination = None
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
|
||||
title = "Dev Options"
|
||||
next = None
|
||||
next_destination = None
|
||||
|
||||
screen = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
selected_button=selected_button,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
# Preserve our scroll position in this Screen so we can return
|
||||
initial_scroll = screen.buttons[0].scroll_y
|
||||
initial_scroll = self.screen.buttons[0].scroll_y
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||
@@ -77,12 +76,12 @@ class SettingsMenuView(View):
|
||||
return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
elif selected_menu_num == len(settings_entries):
|
||||
return next
|
||||
return next_destination
|
||||
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST:
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.IO_TEST:
|
||||
return Destination(IOTestView)
|
||||
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE:
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.DONATE:
|
||||
return Destination(DonateView)
|
||||
|
||||
else:
|
||||
@@ -95,10 +94,10 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
Handles changes to all selection-type settings (Multiselect, SELECT_1,
|
||||
Enabled/Disabled, etc).
|
||||
"""
|
||||
def __init__(self, attr_name: str, parent_initial_scroll: int = 0):
|
||||
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
|
||||
super().__init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
|
||||
self.selected_button = None
|
||||
self.selected_button = selected_button
|
||||
self.parent_initial_scroll = parent_initial_scroll
|
||||
|
||||
|
||||
@@ -111,7 +110,9 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
value, display_name = value
|
||||
else:
|
||||
display_name = value
|
||||
|
||||
button_data.append(display_name)
|
||||
|
||||
if (type(initial_value) == list and value in initial_value) or value == initial_value:
|
||||
checked_buttons.append(i)
|
||||
|
||||
@@ -120,17 +121,18 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
# selected option).
|
||||
self.selected_button = i
|
||||
|
||||
if not self.selected_button:
|
||||
if self.selected_button is None:
|
||||
self.selected_button = 0
|
||||
|
||||
ret_value = settings_screens.SettingsEntryUpdateSelectionScreen(
|
||||
|
||||
ret_value = self.run_screen(
|
||||
settings_screens.SettingsEntryUpdateSelectionScreen,
|
||||
display_name=self.settings_entry.display_name,
|
||||
help_text=self.settings_entry.help_text,
|
||||
button_data=button_data,
|
||||
selected_button=self.selected_button,
|
||||
checked_buttons=checked_buttons,
|
||||
settings_entry_type=self.settings_entry.type,
|
||||
).display()
|
||||
)
|
||||
|
||||
destination = None
|
||||
settings_menu_view_destination = Destination(
|
||||
@@ -178,7 +180,8 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
# All selects stay in place; re-initialize where in the list we left off
|
||||
self.selected_button = ret_value
|
||||
return self.run()
|
||||
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +190,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
****************************************************************************"""
|
||||
class IOTestView(View):
|
||||
def run(self):
|
||||
settings_screens.IOTestScreen().display()
|
||||
self.run_screen(settings_screens.IOTestScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -195,6 +198,6 @@ class IOTestView(View):
|
||||
|
||||
class DonateView(View):
|
||||
def run(self):
|
||||
settings_screens.DonateScreen().display()
|
||||
self.run_screen(settings_screens.DonateScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -7,8 +7,6 @@ from embit.descriptor import Descriptor
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen
|
||||
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
|
||||
@@ -25,32 +23,34 @@ from .view import View, Destination, BackStackView
|
||||
|
||||
|
||||
class ToolsMenuView(View):
|
||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
EXPLORER = "Address Explorer"
|
||||
|
||||
def run(self):
|
||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
EXPLORER = "Address Explorer"
|
||||
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
|
||||
screen = ButtonListScreen(
|
||||
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Tools",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == IMAGE:
|
||||
elif button_data[selected_menu_num] == self.IMAGE:
|
||||
return Destination(ToolsImageEntropyLivePreviewView)
|
||||
|
||||
elif button_data[selected_menu_num] == DICE:
|
||||
elif button_data[selected_menu_num] == self.DICE:
|
||||
return Destination(ToolsDiceEntropyMnemonicLengthView)
|
||||
|
||||
elif button_data[selected_menu_num] == KEYBOARD:
|
||||
elif button_data[selected_menu_num] == self.KEYBOARD:
|
||||
return Destination(ToolsCalcFinalWordNumWordsView)
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
elif button_data[selected_menu_num] == self.EXPLORER:
|
||||
return Destination(ToolsAddressExplorerSelectSourceView)
|
||||
|
||||
|
||||
@@ -232,28 +232,30 @@ class ToolsDiceEntropyEntryView(View):
|
||||
Calc final word Views
|
||||
****************************************************************************"""
|
||||
class ToolsCalcFinalWordNumWordsView(View):
|
||||
TWELVE = "12 words"
|
||||
TWENTY_FOUR = "24 words"
|
||||
|
||||
def run(self):
|
||||
TWELVE = "12 words"
|
||||
TWENTY_FOUR = "24 words"
|
||||
|
||||
button_data = [TWELVE, TWENTY_FOUR]
|
||||
selected_menu_num = ButtonListScreen(
|
||||
button_data = [self.TWELVE, self.TWENTY_FOUR]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Mnemonic Length",
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == TWELVE:
|
||||
elif button_data[selected_menu_num] == self.TWELVE:
|
||||
self.controller.storage.init_pending_mnemonic(12)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
||||
elif button_data[selected_menu_num] == self.TWENTY_FOUR:
|
||||
self.controller.storage.init_pending_mnemonic(24)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
@@ -423,29 +425,27 @@ class ToolsCalcFinalWordDoneView(View):
|
||||
Address Explorer Views
|
||||
****************************************************************************"""
|
||||
class ToolsAddressExplorerSelectSourceView(View):
|
||||
def run(self):
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
|
||||
def run(self):
|
||||
seeds = self.controller.storage.seeds
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
|
||||
button_data.append(SCAN_SEED)
|
||||
button_data.append(SCAN_DESCRIPTOR)
|
||||
button_data.append(TYPE_12WORD)
|
||||
button_data.append(TYPE_24WORD)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Address Explorer",
|
||||
button_data=button_data,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -465,13 +465,13 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
)
|
||||
)
|
||||
|
||||
elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]:
|
||||
elif button_data[selected_menu_num] in [self.SCAN_SEED, self.SCAN_DESCRIPTOR]:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == TYPE_12WORD:
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
@@ -480,6 +480,10 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressTypeView(View):
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
|
||||
|
||||
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
|
||||
"""
|
||||
If the explorer source is a seed, `seed_num` and `script_type` must be
|
||||
@@ -528,29 +532,28 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
def run(self):
|
||||
data = self.controller.address_explorer_data
|
||||
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
button_data = [RECEIVE, CHANGE]
|
||||
|
||||
wallet_descriptor_display_name = None
|
||||
if "wallet_descriptor" in data:
|
||||
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
|
||||
|
||||
script_type = data["script_type"] if "script_type" in data else None
|
||||
|
||||
selected_menu_num = ToolsAddressExplorerAddressTypeScreen(
|
||||
button_data = [self.RECEIVE, self.CHANGE]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ToolsAddressExplorerAddressTypeScreen,
|
||||
button_data=button_data,
|
||||
fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None,
|
||||
wallet_descriptor_display_name=wallet_descriptor_display_name,
|
||||
script_type=script_type,
|
||||
custom_derivation_path=self.custom_derivation,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] in [RECEIVE, CHANGE]:
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE))
|
||||
elif button_data[selected_menu_num] in [self.RECEIVE, self.CHANGE]:
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == self.CHANGE))
|
||||
|
||||
|
||||
|
||||
@@ -565,21 +568,23 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
def run(self):
|
||||
self.loading_screen = None
|
||||
try:
|
||||
addresses = []
|
||||
button_data = []
|
||||
data = self.controller.address_explorer_data
|
||||
addrs_per_screen = 10
|
||||
|
||||
addr_storage_key = "receive_addrs"
|
||||
if self.is_change:
|
||||
addr_storage_key = "change_addrs"
|
||||
addresses = []
|
||||
button_data = []
|
||||
data = self.controller.address_explorer_data
|
||||
addrs_per_screen = 10
|
||||
|
||||
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
|
||||
# We already calculated this range addresses; just retrieve them
|
||||
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
|
||||
addr_storage_key = "receive_addrs"
|
||||
if self.is_change:
|
||||
addr_storage_key = "change_addrs"
|
||||
|
||||
else:
|
||||
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
|
||||
# We already calculated this range of addresses; just retrieve them
|
||||
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
|
||||
|
||||
else:
|
||||
try:
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
|
||||
self.loading_screen.start()
|
||||
|
||||
@@ -608,35 +613,36 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
else:
|
||||
raise Exception("Single sig descriptors not yet supported")
|
||||
|
||||
for i, address in enumerate(addresses):
|
||||
cur_index = i + self.start_index
|
||||
if cur_index < 10:
|
||||
end_digits = -6
|
||||
elif cur_index < 100:
|
||||
end_digits = -5
|
||||
else:
|
||||
end_digits = -4
|
||||
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
|
||||
|
||||
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
|
||||
screen = ButtonListScreen(
|
||||
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
selected_button=self.selected_button_index,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
self.loading_screen.stop()
|
||||
|
||||
selected_menu_num = screen.display()
|
||||
for i, address in enumerate(addresses):
|
||||
cur_index = i + self.start_index
|
||||
|
||||
# Adjust the trailing addr display length based on available room
|
||||
# (the index number will push it out on each order of magnitude)
|
||||
if cur_index < 10:
|
||||
end_digits = -6
|
||||
elif cur_index < 100:
|
||||
end_digits = -5
|
||||
else:
|
||||
end_digits = -4
|
||||
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
|
||||
|
||||
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
selected_button=self.selected_button_index,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -646,7 +652,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen))
|
||||
|
||||
# Preserve the list's current scroll so we can return to the same spot
|
||||
initial_scroll = screen.buttons[0].scroll_y
|
||||
initial_scroll = self.screen.buttons[0].scroll_y
|
||||
|
||||
index = selected_menu_num + self.start_index
|
||||
return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True)
|
||||
@@ -664,10 +670,12 @@ class ToolsAddressExplorerAddressView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
|
||||
QRDisplayScreen(
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
qr_encoder=qr_encoder,
|
||||
).display()
|
||||
)
|
||||
|
||||
# Exiting/Cancelling the QR display screen always returns to the list
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import Type
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.models import Settings
|
||||
|
||||
@@ -56,14 +56,20 @@ class View:
|
||||
self.canvas_width = self.renderer.canvas_width
|
||||
self.canvas_height = self.renderer.canvas_height
|
||||
|
||||
self.buttons = self.controller.buttons
|
||||
self.screen = None
|
||||
|
||||
|
||||
def run_screen(self, Screen_cls: Type[BaseScreen], **kwargs) -> int | str:
|
||||
"""
|
||||
Instantiates the provided Screen_cls and runs its interactive display.
|
||||
Returns the user's input upon completion.
|
||||
"""
|
||||
self.screen = Screen_cls(**kwargs)
|
||||
return self.screen.display()
|
||||
|
||||
|
||||
def run(self, **kwargs):
|
||||
if hasattr(self, "screen"):
|
||||
self.screen.display()
|
||||
else:
|
||||
raise Exception("Must implement in the child class")
|
||||
def run(self, **kwargs) -> 'Destination':
|
||||
raise Exception("Must implement in the child class")
|
||||
|
||||
|
||||
|
||||
@@ -73,10 +79,10 @@ class Destination:
|
||||
Basic struct to pass back to the Controller to tell it which View the user should
|
||||
be presented with next.
|
||||
"""
|
||||
View_cls: View # The target View to route to
|
||||
view_args: dict = None # The input args required to instantiate the target View
|
||||
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
|
||||
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
|
||||
View_cls: Type[View] # The target View to route to
|
||||
view_args: dict = None # The input args required to instantiate the target View
|
||||
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
|
||||
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
@@ -93,12 +99,22 @@ class Destination:
|
||||
return out
|
||||
|
||||
|
||||
def run(self):
|
||||
def _instantiate_view(self):
|
||||
if not self.view_args:
|
||||
# Can't unpack (**) None so we replace with an empty dict
|
||||
self.view_args = {}
|
||||
# Instantiate the `View_cls` and run() it with the `view_args` dict
|
||||
return self.View_cls(**self.view_args).run()
|
||||
|
||||
# Instantiate the `View_cls` with the `view_args` dict
|
||||
self.view = self.View_cls(**self.view_args)
|
||||
|
||||
|
||||
def _run_view(self):
|
||||
return self.view.run()
|
||||
|
||||
|
||||
def run(self):
|
||||
self._instantiate_view()
|
||||
return self._run_view()
|
||||
|
||||
|
||||
def __eq__(self, obj):
|
||||
@@ -121,53 +137,63 @@ class Destination:
|
||||
#
|
||||
#########################################################################################
|
||||
class MainMenuView(View):
|
||||
def run(self):
|
||||
from .seed_views import SeedsMenuView
|
||||
from .settings_views import SettingsMenuView
|
||||
from .scan_views import ScanView
|
||||
from .tools_views import ToolsMenuView
|
||||
from seedsigner.gui.screens import LargeButtonScreen
|
||||
menu_items = [
|
||||
(("Scan", FontAwesomeIconConstants.QRCODE), ScanView),
|
||||
(("Seeds", FontAwesomeIconConstants.KEY), SeedsMenuView),
|
||||
(("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView),
|
||||
(("Settings", FontAwesomeIconConstants.GEAR), SettingsMenuView),
|
||||
]
|
||||
SCAN = ("Scan", FontAwesomeIconConstants.QRCODE)
|
||||
SEEDS = ("Seeds", FontAwesomeIconConstants.KEY)
|
||||
TOOLS = ("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH)
|
||||
SETTINGS = ("Settings", FontAwesomeIconConstants.GEAR)
|
||||
|
||||
screen = LargeButtonScreen(
|
||||
def run(self):
|
||||
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Home",
|
||||
title_font_size=26,
|
||||
button_data=[entry[0] for entry in menu_items],
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
show_power_button=True,
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__POWER_BUTTON:
|
||||
return Destination(PowerOptionsView)
|
||||
|
||||
return Destination(menu_items[selected_menu_num][1])
|
||||
if button_data[selected_menu_num] == self.SCAN:
|
||||
from .scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SEEDS:
|
||||
from .seed_views import SeedsMenuView
|
||||
return Destination(SeedsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TOOLS:
|
||||
from .tools_views import ToolsMenuView
|
||||
return Destination(ToolsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SETTINGS:
|
||||
from .settings_views import SettingsMenuView
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
|
||||
|
||||
class PowerOptionsView(View):
|
||||
RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT)
|
||||
POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF)
|
||||
|
||||
def run(self):
|
||||
RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT)
|
||||
POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF)
|
||||
button_data = [RESET, POWER_OFF]
|
||||
selected_menu_num = LargeButtonScreen(
|
||||
button_data = [self.RESET, self.POWER_OFF]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Reset / Power",
|
||||
show_back_button=True,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == RESET:
|
||||
elif button_data[selected_menu_num] == self.RESET:
|
||||
return Destination(RestartView)
|
||||
|
||||
elif button_data[selected_menu_num] == POWER_OFF:
|
||||
elif button_data[selected_menu_num] == self.POWER_OFF:
|
||||
return Destination(PowerOffView)
|
||||
|
||||
|
||||
@@ -176,7 +202,7 @@ class RestartView(View):
|
||||
def run(self):
|
||||
thread = RestartView.DoResetThread()
|
||||
thread.start()
|
||||
ResetScreen().display()
|
||||
self.run_screen(ResetScreen)
|
||||
|
||||
|
||||
class DoResetThread(BaseThread):
|
||||
@@ -200,12 +226,12 @@ class RestartView(View):
|
||||
class PowerOffView(View):
|
||||
def run(self):
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
PowerOffNotRequiredScreen().display()
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
else:
|
||||
thread = PowerOffView.PowerOffThread()
|
||||
thread.start()
|
||||
PowerOffScreen().display()
|
||||
self.run_screen(PowerOffScreen)
|
||||
|
||||
|
||||
class PowerOffThread(BaseThread):
|
||||
@@ -223,30 +249,32 @@ class NotYetImplementedView(View):
|
||||
Temporary View to use during dev.
|
||||
"""
|
||||
def run(self):
|
||||
WarningScreen(
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Work In Progress",
|
||||
status_headline="Not Yet Implemented",
|
||||
text="This is still on our to-do list!",
|
||||
button_data=["Back to Main Menu"],
|
||||
).display()
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
|
||||
|
||||
class UnhandledExceptionView(View):
|
||||
def __init__(self, error: List[str]):
|
||||
def __init__(self, error: list[str]):
|
||||
self.error = error
|
||||
|
||||
|
||||
def run(self):
|
||||
DireWarningScreen(
|
||||
self.run_screen(
|
||||
DireWarningScreen,
|
||||
title="System Error",
|
||||
status_headline=self.error[0],
|
||||
text=self.error[1] + "\n" + self.error[2],
|
||||
button_data=["OK"],
|
||||
show_back_button=False,
|
||||
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
|
||||
).display()
|
||||
)
|
||||
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
+8
-2
@@ -1,7 +1,13 @@
|
||||
# Running Tests
|
||||
|
||||
You must install the test-runner dependencies:
|
||||
```
|
||||
The tests are designed to be run on non-Raspi hardware.
|
||||
|
||||
On your testing machine you'll have to install:
|
||||
```bash
|
||||
# general dependencies
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# test suite dependencies
|
||||
pip3 install -r tests/requirements.txt
|
||||
```
|
||||
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from mock import MagicMock, patch
|
||||
from typing import Callable
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.gui.renderer'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models import Settings
|
||||
from seedsigner.views.view import Destination, MainMenuView, View
|
||||
|
||||
|
||||
|
||||
class BaseTest:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
# Ensure there are no on-disk artifacts after running tests.
|
||||
Settings.SETTINGS_FILENAME = "settings-test.json"
|
||||
|
||||
# Mock out the loading screen so it can't spawn. View classes must import locally!
|
||||
patch('seedsigner.gui.screens.screen.LoadingScreenThread').start()
|
||||
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
pass
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_settings(cls):
|
||||
""" Wipe and re-initialize the Settings singleton """
|
||||
Settings._instance = None
|
||||
BaseTest.remove_settings()
|
||||
|
||||
|
||||
@classmethod
|
||||
def remove_settings(cls):
|
||||
""" If settings were written to disk, delete """
|
||||
import os
|
||||
try:
|
||||
if os.path.exists(Settings.SETTINGS_FILENAME):
|
||||
os.remove(Settings.SETTINGS_FILENAME)
|
||||
except:
|
||||
print(f"{Settings.SETTINGS_FILENAME} could not be removed")
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_controller(cls):
|
||||
""" Wipe and re-initialize the Controller singleton """
|
||||
Controller._instance = None
|
||||
Controller.configure_instance()
|
||||
|
||||
|
||||
def setup_method(self):
|
||||
""" Guarantee a clean/default Controller and Settings state for each test case """
|
||||
BaseTest.reset_controller()
|
||||
BaseTest.reset_settings()
|
||||
self.controller = Controller.get_instance()
|
||||
self.settings = Settings.get_instance()
|
||||
|
||||
|
||||
def teardown_method(self):
|
||||
BaseTest.remove_settings()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowStep:
|
||||
"""
|
||||
Trivial helper class to express FlowTest sequences below.
|
||||
|
||||
* expected_view: verify that the current step in the sequence instantiates the right View.
|
||||
* before_run: function that takes a View instance as an arg and modifies it before running the View.
|
||||
* screen_return_value: mocked Screen interaction result: raw return value as if from the Screen.
|
||||
* button_data_selection: mocked Screen interaction result: the View.button_data value of the desired option.
|
||||
* is_redirect: expects the Destination to specify `skip_current_view=True`.
|
||||
"""
|
||||
expected_view: type[View] = None
|
||||
before_run: Callable[[View], None] = None
|
||||
screen_return_value: int | str = None
|
||||
button_data_selection: str | tuple = None
|
||||
is_redirect: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.screen_return_value is not None and self.button_data_selection is not None:
|
||||
raise Exception("Can't specify both `screen_return_value` and `button_data_selection`")
|
||||
|
||||
|
||||
|
||||
class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException):
|
||||
""" The FlowStep's button_data_selection value was not found in the View's button_data """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTestUnexpectedViewException(FlowBasedTestException):
|
||||
""" The Controller's current View did not match the expected View in the FlowStep sequence """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTestRunScreenNotExecutedException(FlowBasedTestException):
|
||||
""" The View's run_screen() method was not called but the FlowStep expected it to need user input """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTest(BaseTest):
|
||||
""" Base class for any tests that do flow-based testing """
|
||||
|
||||
def stop_test(self):
|
||||
""" Raise our special Exception to stop the test """
|
||||
raise StopFlowBasedTest()
|
||||
|
||||
|
||||
def run_sequence(self, sequence: list[FlowStep], initial_destination_view_args: dict = None) -> None:
|
||||
"""
|
||||
Run a pre-set sequence of Views w/manually-specified return values in order to test
|
||||
the Controller's flow control logic and the routing from View to View.
|
||||
"""
|
||||
with patch("seedsigner.views.view.Destination._run_view", autospec=True) as mock_run_view:
|
||||
with patch("seedsigner.views.view.View.run_screen", autospec=True) as mock_run_screen:
|
||||
def run_view(destination: Destination, *args, **kwargs):
|
||||
""" Replaces Destination._run_view() """
|
||||
if len(sequence) == 0:
|
||||
self.stop_test()
|
||||
|
||||
cur_flow_step = sequence[0]
|
||||
|
||||
# Verify that the View class specified in the test sequence matches the
|
||||
# View class that is being run.
|
||||
if destination.View_cls != cur_flow_step.expected_view:
|
||||
raise FlowTestUnexpectedViewException(f"Expected {cur_flow_step.expected_view}, got {destination.View_cls}")
|
||||
|
||||
# Run the optional pre-run function to modify the View.
|
||||
if cur_flow_step.before_run:
|
||||
cur_flow_step.before_run(destination.view)
|
||||
|
||||
if cur_flow_step.is_redirect:
|
||||
# The current View is going to auto-redirect without calling run_screen(),
|
||||
# so we need to remove the current step from the sequence before the
|
||||
# View.run() call below.
|
||||
sequence.pop(0)
|
||||
|
||||
# Some Views reach into their Screen's variables directly (e.g.
|
||||
# Screen.buttons to preserve the scroll position), so we need to mock out the
|
||||
# Screen instance that is created by the View.
|
||||
destination.view.screen = MagicMock()
|
||||
|
||||
prev_mock_run_screen_call_count = mock_run_screen.call_count
|
||||
|
||||
# Run the View (with our mocked run_screen) and get the next Destination that results
|
||||
destination = destination.view.run()
|
||||
|
||||
if (cur_flow_step.button_data_selection or cur_flow_step.screen_return_value is not None) and mock_run_screen.call_count == prev_mock_run_screen_call_count:
|
||||
# The FlowStep was expecting some kind of user interaction, but the View
|
||||
# never called run_screen().
|
||||
raise FlowTestRunScreenNotExecutedException(f"View.run_screen() was not run for {destination.View_cls.__name__}")
|
||||
|
||||
return destination
|
||||
|
||||
def run_screen(view: View, *args, **kwargs):
|
||||
""" Replaces View.run_screen() """
|
||||
# Return the return value specified in the test sequence and
|
||||
# remove the completed test step from the sequence.
|
||||
flow_step = sequence.pop(0)
|
||||
|
||||
if flow_step.button_data_selection:
|
||||
# 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,
|
||||
# including the button_data kwarg.
|
||||
if "button_data" in kwargs:
|
||||
if flow_step.button_data_selection not in kwargs.get("button_data") and flow_step.button_data_selection not in [RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON]:
|
||||
raise FlowTestInvalidButtonDataSelectionException(f"'{flow_step.button_data_selection}' not found in button_data: {kwargs.get('button_data')}")
|
||||
return kwargs.get("button_data").index(flow_step.button_data_selection)
|
||||
else:
|
||||
raise Exception(f"Can't specify `FlowStep.button_data_selection` if `button_data` isn't a kwarg in {view.__class__.__name__}'s run_screen()")
|
||||
|
||||
elif type(flow_step.screen_return_value) in [StopFlowBasedTest, FlowBasedTestException]:
|
||||
raise flow_step.screen_return_value
|
||||
|
||||
return flow_step.screen_return_value
|
||||
|
||||
# Mock out the Destination._run_view() method so we can verify the View class
|
||||
# that is specified in the test sequence and then run the View.
|
||||
mock_run_view.side_effect = run_view
|
||||
|
||||
# Mock out the View.run_screen() method so we can provide the
|
||||
# return value that is specified in the test sequence.
|
||||
mock_run_screen.side_effect = run_screen
|
||||
|
||||
# Start the Controller with the first View_cls specified in the test sequence
|
||||
if sequence[0].expected_view != MainMenuView:
|
||||
initial_destination = Destination(sequence[0].expected_view, view_args=initial_destination_view_args)
|
||||
else:
|
||||
initial_destination = None
|
||||
|
||||
# Start the Controller and run the sequence
|
||||
Controller.get_instance().start(initial_destination=initial_destination)
|
||||
+91
-93
@@ -1,117 +1,115 @@
|
||||
import configparser
|
||||
import pytest
|
||||
from mock import MagicMock
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
# Must import this before the Controller
|
||||
from base import BaseTest
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def reset_controller():
|
||||
"""fixture to setup, then yield to run test, then tear down"""
|
||||
class TestController(BaseTest):
|
||||
|
||||
# setup
|
||||
Controller.configure_instance(disable_hardware=True)
|
||||
def test_reset_controller(self):
|
||||
""" The reset_controller util should completely reset the Controller singleton """
|
||||
controller = Controller.get_instance()
|
||||
controller.address_explorer_data = "foo"
|
||||
|
||||
# yield to run a single test
|
||||
yield
|
||||
|
||||
# tear down
|
||||
MicroSD._instance = None
|
||||
Controller._instance = None
|
||||
|
||||
|
||||
def test_singleton_init_fails(reset_controller):
|
||||
""" The Controller should not allow any code to instantiate it via Controller() """
|
||||
with pytest.raises(Exception):
|
||||
c = Controller()
|
||||
BaseTest.reset_controller()
|
||||
controller = Controller.get_instance()
|
||||
assert controller.address_explorer_data is None
|
||||
|
||||
|
||||
def test_singleton_get_instance_preserves_state(reset_controller):
|
||||
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
|
||||
|
||||
# Initialize the instance and verify that it read the config settings
|
||||
controller = Controller.get_instance()
|
||||
assert controller.unverified_address is None
|
||||
|
||||
# Change a value in the instance...
|
||||
controller.unverified_address = "123abc"
|
||||
|
||||
# ...get a new copy of the instance and confirm change
|
||||
controller = Controller.get_instance()
|
||||
assert controller.unverified_address == "123abc"
|
||||
def test_singleton_init_fails(self):
|
||||
""" The Controller should not allow any code to instantiate it via Controller() """
|
||||
with pytest.raises(Exception):
|
||||
c = Controller()
|
||||
|
||||
|
||||
def test_handle_exception(reset_controller):
|
||||
""" Handle exceptions that get caught by the controller """
|
||||
def test_handle_exception(reset_controller):
|
||||
""" Handle exceptions that get caught by the controller """
|
||||
|
||||
def process_exception_asserting_valid_error(exception_type, exception_msg=None):
|
||||
"""
|
||||
Exceptions caught by the controller are forwarded to the
|
||||
UnhandledExceptionView with view_args["error"] being a list
|
||||
of three strings, ie: [exception_type, line_info, exception_msg]
|
||||
"""
|
||||
try:
|
||||
def process_exception_asserting_valid_error(exception_type, exception_msg=None):
|
||||
"""
|
||||
Exceptions caught by the controller are forwarded to the
|
||||
UnhandledExceptionView with view_args["error"] being a list
|
||||
of three strings, ie: [exception_type, line_info, exception_msg]
|
||||
"""
|
||||
try:
|
||||
if exception_msg:
|
||||
raise exception_type(exception_msg)
|
||||
else:
|
||||
raise exception_type()
|
||||
except Exception as e:
|
||||
error = controller.handle_exception(e).view_args["error"]
|
||||
|
||||
# assert that error structure is valid
|
||||
assert len(error) == 3
|
||||
assert error[0] in str(exception_type)
|
||||
assert type(error[1]) == str
|
||||
if exception_msg:
|
||||
raise exception_type(exception_msg)
|
||||
assert exception_msg in error[2]
|
||||
else:
|
||||
raise exception_type()
|
||||
except Exception as e:
|
||||
error = controller.handle_exception(e).view_args["error"]
|
||||
assert error[2] == ""
|
||||
|
||||
# assert that error structure is valid
|
||||
assert len(error) == 3
|
||||
assert error[0] in str(exception_type)
|
||||
assert type(error[1]) == str
|
||||
if exception_msg:
|
||||
assert exception_msg in error[2]
|
||||
else:
|
||||
assert error[2] == ""
|
||||
# Initialize the controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Initialize the controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
exception_tests = [
|
||||
# exceptions with an exception_msg
|
||||
(Exception, "foo"),
|
||||
(KeyError, "key not found"),
|
||||
# exceptions without an exception_msg
|
||||
(Exception, ""),
|
||||
(Exception, None),
|
||||
]
|
||||
|
||||
for exception_type, exception_msg in exception_tests:
|
||||
process_exception_asserting_valid_error(exception_type, exception_msg)
|
||||
exception_tests = [
|
||||
# exceptions with an exception_msg
|
||||
(Exception, "foo"),
|
||||
(KeyError, "key not found"),
|
||||
# exceptions without an exception_msg
|
||||
(Exception, ""),
|
||||
(Exception, None),
|
||||
]
|
||||
|
||||
for exception_type, exception_msg in exception_tests:
|
||||
process_exception_asserting_valid_error(exception_type, exception_msg)
|
||||
|
||||
|
||||
def test_missing_settings_get_defaults(reset_controller):
|
||||
""" Should gracefully handle all missing fields from `settings.json` """
|
||||
def test_singleton_get_instance_preserves_state(self):
|
||||
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
|
||||
|
||||
controller = Controller.get_instance()
|
||||
# Initialize the instance and verify that it read the config settings
|
||||
controller = Controller.get_instance()
|
||||
assert controller.unverified_address is None
|
||||
|
||||
# Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
|
||||
# Change a value in the instance...
|
||||
controller.unverified_address = "123abc"
|
||||
|
||||
# Advanced Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
|
||||
# ...get a new copy of the instance and confirm change
|
||||
controller = Controller.get_instance()
|
||||
assert controller.unverified_address == "123abc"
|
||||
|
||||
# Hidden Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189
|
||||
|
||||
def test_missing_settings_get_defaults(self):
|
||||
""" Should gracefully handle all missing fields from `settings.json` """
|
||||
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
|
||||
|
||||
# Advanced Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
|
||||
|
||||
# Hidden Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import pytest
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException, FlowTestRunScreenNotExecutedException
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView
|
||||
from seedsigner.views.view import MainMenuView, PowerOptionsView, UnhandledExceptionView
|
||||
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
|
||||
|
||||
|
||||
|
||||
class TestFlowTest(FlowTest):
|
||||
|
||||
def test_simple_flow(self):
|
||||
"""
|
||||
Basic test to ensure the FlowTest can flow through a sequence of Views and
|
||||
terminate via the StopControllerCommand.
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView, button_data_selection=ToolsCalcFinalWordNumWordsView.TWELVE),
|
||||
FlowStep(SeedMnemonicEntryView),
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestUnexpectedViewException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the next
|
||||
View in the sequence is not the expected View.
|
||||
"""
|
||||
with pytest.raises(FlowTestUnexpectedViewException):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(ToolsMenuView), # <-- Wrong target View! Should raise an AssertionError.
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestInvalidButtonDataSelectionException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the
|
||||
specified button_data_selection in invalid.
|
||||
"""
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection="this is not a real button option!"),
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestRunScreenNotExecutedException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestRunScreenNotExecutedException if the next
|
||||
View in the sequence doesn't call its View.run_screen().
|
||||
"""
|
||||
# Disable dire warnings so that the SeedWordsWarningView won't execute its run_screen()
|
||||
self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED)
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=["bacon"] * 24))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
with pytest.raises(FlowTestRunScreenNotExecutedException):
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
|
||||
FlowStep(SeedBackupView, button_data_selection=SeedBackupView.VIEW_WORDS),
|
||||
FlowStep(SeedWordsWarningView, screen_return_value=0),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_before_run_executes(self):
|
||||
"""
|
||||
Ensure that the FlowTest can execute a function before running a View.
|
||||
"""
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
def test_back_button_flow(self):
|
||||
"""
|
||||
Ensure that the FlowTest works correctly with the Controller and its BackStack.
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_initial_destination(self):
|
||||
"""
|
||||
Ensure that the FlowTest can start from a View other than MainMenuView.
|
||||
"""
|
||||
# Don't have to start at the MainMenuView; can jump straight in
|
||||
self.run_sequence([
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView),
|
||||
])
|
||||
|
||||
# And again, but this time with a View that requires input view_args
|
||||
self.reset_controller()
|
||||
self.controller = Controller.get_instance()
|
||||
|
||||
# Load a seed into the Controller
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
self.controller.storage.set_pending_seed(seed)
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
|
||||
FlowStep(SeedBackupView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_raise_exception_via_screen_return_value(self):
|
||||
"""
|
||||
Ensure that the FlowTest can raise an exception via the screen_return_value.
|
||||
"""
|
||||
# A generic Exception should be caught by the Controller and routed to the
|
||||
# UnhandledExceptionView.
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=Exception("Test exception")),
|
||||
FlowStep(UnhandledExceptionView),
|
||||
])
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
|
||||
class TestPSBTFlows(FlowTest):
|
||||
|
||||
def test_scan_psbt_first_then_correct_seedqr_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a PSBT should enter the PSBTSelectSeedView flow
|
||||
when Scan a Seed is selected from PSBTSelectSeedView it should enter the ScanView flow
|
||||
when a SeedQR is scanned it should enter the PSBTOverviewView flow
|
||||
since the PSBT has change no warning is displayed and it should enter the PSBTMathView flow
|
||||
since the PSBT is not a self transfer it should enter the PSBTAddressDetailsView flow
|
||||
"""
|
||||
def load_psbt_into_decoder(view: scan_views.ScanView):
|
||||
"""
|
||||
PSBT Tx and Wallet Details
|
||||
- Single Sig Wallet P2WPKH (Native Segwit) with no passphrase
|
||||
- Regtest c751dc07 m/84'/1'/0' tpubDDZBrnxMxbVzqt8EoEiABPxeKzFWma5pra5UEbg3Wst1hrwr6feuvcy7Sov7cpuYx94ypuy1PQ9NDNoQagFs37wGALzLb5Ei3FvyJWPPPKZ
|
||||
- 2 Inputs
|
||||
- 56,522,834 sats
|
||||
- 1,990,245,069 sats
|
||||
- 4 Outputs
|
||||
- 1 Output to another wallet (bcrt1q7cw0wzy8g6mq5qvkpvhnk5gsps5ncy3srp0n2j) of 123,456 sats
|
||||
- 3 Outputs change
|
||||
- 3 outputs to emulate a fake mix to increase privacy
|
||||
- Change addresses are index 1/7, 1/8, 1/9
|
||||
- 1/7 address bcrt1q53j0xwuskuf5gnvynadh0hlazyy8srydlucrhg with amount 123,456 sats
|
||||
- 1/8 address bcrt1q5gtw3zfp4cx67yk5q42q6j6rfza8aqcwpyyslv with amount 1,990,121,477 sats
|
||||
- 1/9 address bcrt1q9rrg7399m43cn0yg4tz0v0ate89jgf2d6kpz7v with amount 56,399,242 sats
|
||||
- Fee 272 sats
|
||||
"""
|
||||
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==")
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("080115060387063104071857067618681125136207731354")
|
||||
|
||||
self.run_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.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(psbt_views.PSBTOverviewView),
|
||||
FlowStep(psbt_views.PSBTMathView),
|
||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=psbt_views.PSBTAddressDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
||||
FlowStep(MainMenuView)
|
||||
])
|
||||
@@ -0,0 +1,290 @@
|
||||
# Must import test base before the Controller
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
import pytest
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import seed_views, scan_views
|
||||
|
||||
|
||||
|
||||
class TestSeedFlows(FlowTest):
|
||||
|
||||
def test_scan_seedqr_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the
|
||||
Finalize Seed flow and end at the SeedOptionsView.
|
||||
"""
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
])
|
||||
|
||||
|
||||
def test_mnemonic_entry_flow(self):
|
||||
"""
|
||||
Manually entering a mnemonic should land at the Finalize Seed flow and end at
|
||||
the SeedOptionsView.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic):
|
||||
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_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
|
||||
]
|
||||
|
||||
# Now add each manual word entry step
|
||||
for word in mnemonic:
|
||||
sequence.append(
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
|
||||
)
|
||||
|
||||
# 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),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
# Test data from iancoleman.io; 12- and 24-word mnemonic
|
||||
test_with_mnemonic("tone flat shed cool census soul paddle boy flight fantasy stem social".split())
|
||||
|
||||
BaseTest.reset_controller()
|
||||
|
||||
test_with_mnemonic("cotton artefact spy mind wing there echo steak child oak awful host despair online bicycle divorce middle firm diamond rare execute chimney almost hollow".split())
|
||||
|
||||
|
||||
def test_invalid_mnemonic(self):
|
||||
""" Should be able to go back and edit or discard an invalid mnemonic """
|
||||
# Test data from iancoleman.io
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
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_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
|
||||
]
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zoo"), # But finish with an INVALID checksum word
|
||||
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.EDIT),
|
||||
]
|
||||
|
||||
# Restarts from first word
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zebra"), # provide yet another invalid checksum word
|
||||
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
def test_export_xpub_standard_flow(self):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
"""
|
||||
|
||||
def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple):
|
||||
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=sig_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]),
|
||||
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),
|
||||
]
|
||||
)
|
||||
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# these are lists of (constant_value, display_name) tuples
|
||||
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
|
||||
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
|
||||
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
|
||||
|
||||
# enable non-defaults so they're available in views
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types])
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators])
|
||||
|
||||
# exhaustively test flows thru standard sig_types, script_types, and coordinators
|
||||
for sig_tuple in sig_types:
|
||||
for script_tuple in script_types:
|
||||
for coord_tuple in coordinators:
|
||||
# skip custom derivation
|
||||
if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION:
|
||||
continue
|
||||
# skip multisig taproot
|
||||
elif sig_tuple[0] == SettingsConstants.MULTISIG and script_tuple[0] == SettingsConstants.TAPROOT:
|
||||
continue
|
||||
else:
|
||||
print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, coord_tuple))
|
||||
flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple)
|
||||
|
||||
|
||||
def test_export_xpub_disabled_not_available_flow(self):
|
||||
"""
|
||||
If sig_type/script_type/coordinator disabled, then these options are not available
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# these are lists of (constant_value, display_name) tuples
|
||||
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
|
||||
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
|
||||
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
|
||||
|
||||
# these are the disabled types that we will be testing
|
||||
disabled_sig = SettingsConstants.MULTISIG
|
||||
disabled_script = SettingsConstants.TAPROOT
|
||||
disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK
|
||||
|
||||
# enable all but our target disabled type
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types if x!=disabled_sig])
|
||||
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),
|
||||
]
|
||||
)
|
||||
|
||||
# test that taproot is not an option via exception raised when choice is taproot
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException) 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, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=disabled_script),
|
||||
]
|
||||
)
|
||||
|
||||
# test that nunchuk is not an option via exception raised when choice is nunchuk
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException) 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, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=disabled_coord),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_export_xpub_custom_derivation_flow(self):
|
||||
"""
|
||||
Export XPUB flow for custom derivation finishes at MainMenuView
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# enable custom derivation script_type setting (plus at least one more for a choice)
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [
|
||||
SettingsConstants.NATIVE_SEGWIT,
|
||||
SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.CUSTOM_DERIVATION
|
||||
])
|
||||
|
||||
# get display names to access button choices in the views (ugh: hardcoding, is there a better way?)
|
||||
sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig
|
||||
script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation
|
||||
coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter
|
||||
|
||||
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=sig_type),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_type),
|
||||
FlowStep(seed_views.SeedExportXpubCustomDerivationView, screen_return_value="m/0'/0'"),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coordinator),
|
||||
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_export_xpub_skip_non_option_flow(self):
|
||||
"""
|
||||
Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or coordinators
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# exclusively set only one choice for each of sig_types, script_types and coordinators
|
||||
self.settings.update({
|
||||
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
|
||||
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
|
||||
}, disable_missing_entries=False)
|
||||
|
||||
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, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, is_redirect=True),
|
||||
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
|
||||
remove the in-memory seed from the Controller.
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.DISCARD),
|
||||
FlowStep(seed_views.SeedDiscardView, button_data_selection=seed_views.SeedDiscardView.DISCARD),
|
||||
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),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.models import SettingsDefinition
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import settings_views
|
||||
|
||||
|
||||
|
||||
class TestSettingsFlows(FlowTest):
|
||||
|
||||
def test_persistent_settings(self):
|
||||
""" Basic flow from MainMenuView to enable/disable persistent settings """
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
|
||||
# No settings file should exist before we enable persistent settings
|
||||
assert os.path.exists(Settings.SETTINGS_FILENAME) == False
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
# Settings file should now exist
|
||||
assert os.path.exists(Settings.SETTINGS_FILENAME) == True
|
||||
|
||||
|
||||
def test_multiselect(self):
|
||||
""" Multiselect Settings options should stay in-place; requires BACK to exit. """
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_io_test(self):
|
||||
""" Basic flow from MainMenuView to I/O Test View """
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.IO_TEST),
|
||||
FlowStep(settings_views.IOTestView),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_donate(self):
|
||||
""" Basic flow from MainMenuView to Donate View """
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.DONATE),
|
||||
FlowStep(settings_views.DonateView),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
@@ -0,0 +1,72 @@
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, seed_views, tools_views
|
||||
|
||||
|
||||
|
||||
class TestToolsFlows(FlowTest):
|
||||
|
||||
def test_addressexplorer_flow(self):
|
||||
"""
|
||||
Test the simplest AddressExplorer flow when a seed is already loaded.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
controller.storage.set_pending_seed(seed)
|
||||
controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressView), # runs until dismissed; no ret value
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView),
|
||||
])
|
||||
|
||||
|
||||
def test_addressexplorer_loadseed_sideflow(self):
|
||||
"""
|
||||
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")
|
||||
|
||||
# Finalize the new seed w/out passphrase
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
])
|
||||
|
||||
assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
|
||||
# Finalize the new seed w/passphrase
|
||||
self.run_sequence(
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
from mock import patch
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView
|
||||
from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View
|
||||
|
||||
|
||||
|
||||
class TestViewFlows(FlowTest):
|
||||
|
||||
def test_restart_flow(self):
|
||||
"""
|
||||
Basic flow from MainMenuView to RestartView
|
||||
"""
|
||||
with patch('seedsigner.views.view.RestartView.DoResetThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.RESET),
|
||||
FlowStep(RestartView),
|
||||
])
|
||||
|
||||
|
||||
def test_power_off_flow(self):
|
||||
"""
|
||||
Basic flow from MainMenuView to PowerOffView
|
||||
"""
|
||||
with patch('seedsigner.views.view.PowerOffView.PowerOffThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView),
|
||||
])
|
||||
|
||||
# And again, but this time as if we were in the SeedSigner OS
|
||||
Settings.HOSTNAME = Settings.SEEDSIGNER_OS
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView), # returns BackStackView
|
||||
FlowStep(PowerOptionsView),
|
||||
])
|
||||
|
||||
|
||||
def test_not_yet_implemented_flow(self):
|
||||
"""
|
||||
Run an incomplete View that returns None and ensure that we get the NotYetImplementedView
|
||||
"""
|
||||
class IncompleteView(View):
|
||||
def run(self):
|
||||
self.run_screen(None)
|
||||
return None
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(IncompleteView),
|
||||
FlowStep(NotYetImplementedView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_unhandled_exception_flow(self):
|
||||
"""
|
||||
Basic flow from any arbitrary View to the UnhandledExceptionView
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView, screen_return_value=Exception("Test exception")), # <-- force an exception
|
||||
FlowStep(UnhandledExceptionView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
@@ -0,0 +1,16 @@
|
||||
from base import BaseTest
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
class TestSettings(BaseTest):
|
||||
|
||||
def test_reset_settings(self):
|
||||
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED)
|
||||
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED
|
||||
|
||||
BaseTest.reset_settings()
|
||||
settings = Settings.get_instance()
|
||||
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
Reference in New Issue
Block a user