Tweaks to passphrase flow, settings; SeedWordsView now highlighting four words at a time

This commit is contained in:
kdmukai
2022-03-10 14:41:12 -06:00
parent 708ce14976
commit 63e4de8662
10 changed files with 168 additions and 160 deletions
+1
View File
@@ -53,6 +53,7 @@ All of the above!
These features will not be included in the initial v0.5.0 release and will have varying degrees of priority for subsequent releases (or possibly not at all).
* Multisig wallet descriptor QR scan(?) and addr verification(?)
* Sign taproot txs
* Multi-language support (Transifex free for open source projects)
* Multisig: sign PSBT with multiple keys at once.
* Custom OS, possibly with swappable SD card PSBT and multisig wallet descriptor storage
+2 -2
View File
@@ -517,6 +517,7 @@ class LargeButtonScreen(BaseTopNavScreen):
button_font_name: str = GUIConstants.BUTTON_FONT_NAME
button_font_size: int = 20
button_selected_color: str = GUIConstants.ACCENT_COLOR
selected_button: int = 0
def __post_init__(self):
super().__post_init__()
@@ -570,8 +571,7 @@ class LargeButtonScreen(BaseTopNavScreen):
if i == 1:
button_start_y += button_height + GUIConstants.COMPONENT_PADDING
self.buttons[0].is_selected = True
self.selected_button = 0
self.buttons[self.selected_button].is_selected = True
def _run(self):
+37 -38
View File
@@ -443,43 +443,49 @@ class SeedOptionsScreen(ButtonListScreen):
@dataclass
class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
title: str = "Seed Words"
seed: Seed = None
is_first_page: bool = True
page_index: int = 0
num_pages: int = 3
is_bottom_list: bool = True
warning_color: str = GUIConstants.DIRE_WARNING_COLOR
def __post_init__(self):
self.title = f"Seed Words: {self.page_index+1}/{self.num_pages}"
super().__post_init__()
# Can only render 12 words per screen
# Can only render 4 words per screen
words_per_page = 4
mnemonic = self.seed.mnemonic_display_list
if len(mnemonic) == 12 or self.is_first_page:
self.mnemonic = mnemonic[:12]
else:
self.mnemonic = mnemonic[12:]
# Slice the mnemonic to our current 4-word section
self.mnemonic = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page]
self.body_x = 0
self.body_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING / 2)
self.body_height = self.buttons[0].screen_y - self.body_y
# Have to supersample the whole body since it's all at the small font size
supersampling_factor = 2
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, 16 * supersampling_factor)
supersampling_factor = 1
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2) * supersampling_factor)
# Calc vertical placement for the numbers
(number_x, number_y) = calc_text_centering(
font=font,
text="1234567890",
is_text_centered=True,
total_width=20 * supersampling_factor,
total_height=20 * supersampling_factor
)
number_box_x = GUIConstants.EDGE_PADDING * supersampling_factor
number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor
number_box_width = 20 * supersampling_factor
number_box_height = 20 * supersampling_factor
# Calc horizontal center based on longest word
max_word_width = 0
for word in self.mnemonic:
(left, top, right, bottom) = font.getbbox(word, anchor="ls")
if right > max_word_width:
max_word_width = right
# Measure the max digit height for the numbering boxes, from baseline
number_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE * supersampling_factor)
(left, top, right, bottom) = number_font.getbbox("24", anchor="ls")
number_height = -1 * top
number_width = right
number_box_width = number_width + int(GUIConstants.COMPONENT_PADDING/2 * supersampling_factor)
number_box_height = number_box_width
number_box_x = int((self.canvas_width * supersampling_factor - number_box_width - GUIConstants.COMPONENT_PADDING*supersampling_factor - max_word_width))/2
number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor
# Set up our temp supersampled rendering surface
self.body_img = Image.new(
@@ -490,37 +496,30 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
draw = ImageDraw.Draw(self.body_img)
for index, word in enumerate(self.mnemonic):
if index == 6:
# Start of the second column of words
number_box_x = (int(self.canvas_width / 2) + 4) * supersampling_factor
number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor
draw.rounded_rectangle(
(number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height),
fill="#202020",
radius=5 * supersampling_factor
)
if self.is_first_page:
number_str = str(index + 1)
else:
number_str = str(index + 1 + 12)
tw, th = font.getsize(number_str)
baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2)
draw.text(
(number_box_x + int((number_box_width - tw) / 2), number_box_y + number_y),
font=font,
text=number_str,
fill="#0084ff"
(number_box_x + int(number_box_width/2), baseline_y),
font=number_font,
text=str(self.page_index * words_per_page + index + 1),
fill="#0084ff",
anchor="ms" # Middle (centered), baSeline
)
# Now draw the word
draw.text(
(number_box_x + number_box_width + (4 * supersampling_factor), number_box_y + number_y),
(number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y),
font=font,
text=word,
fill=GUIConstants.BODY_FONT_COLOR
fill=GUIConstants.BODY_FONT_COLOR,
anchor="ls", # Left, baSeline
)
number_box_y += number_box_height + (4 * supersampling_factor)
number_box_y += number_box_height + (int(1.5*GUIConstants.COMPONENT_PADDING) * supersampling_factor)
# Resize to target and sharpen final image
self.body_img = self.body_img.resize((self.canvas_width, self.body_height), Image.LANCZOS)
+1 -1
View File
@@ -42,7 +42,7 @@ class HardwareButtons(Singleton):
cls._instance.cur_input = None # Track which direction or button was last pressed
cls._instance.cur_input_started = None # Track when that input began
cls._instance.last_input_time = int(time.time() * 1000) # How long has it been since the last input?
cls._instance.first_repeat_threshold = 175 # Long-press time required before returning continuous input
cls._instance.first_repeat_threshold = 225 # Long-press time required before returning continuous input
cls._instance.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold
return cls._instance
+2
View File
@@ -74,6 +74,8 @@ class Settings(Singleton):
if type(new_settings[entry.attr_name]) == str:
# Break comma-separated SettingsQR input into List
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
# TODO: If value is not in entry.selection_options...
# Can't just merge the _data dict; have to replace keys they have in common
+21 -1
View File
@@ -13,6 +13,14 @@ class SettingsConstants:
(OPTION__ENABLED, "Enabled"),
(OPTION__DISABLED, "Disabled"),
]
OPTIONS__PROMPT_REQUIRED_DISABLED = [
(OPTION__PROMPT, "Prompt"),
(OPTION__REQUIRED, "Required"),
(OPTION__DISABLED, "Disabled"),
]
OPTIONS__ENABLED_DISABLED_REQUIRED = OPTIONS__ENABLED_DISABLED +[
(OPTION__REQUIRED, "Required"),
]
OPTIONS__ENABLED_DISABLED_PROMPT = OPTIONS__ENABLED_DISABLED + [
(OPTION__PROMPT, "Prompt"),
]
@@ -244,6 +252,18 @@ class SettingsEntry:
return display_name
def get_selection_option_value_by_display_name(self, display_name: str):
for option in self.selection_options:
if type(option) == tuple:
option_value = option[0]
option_display_name = option[1]
else:
option_value = option
option_display_name = option
if option_display_name == display_name:
return option_value
def to_dict(self) -> dict:
if self.selection_options:
selection_options = []
@@ -377,7 +397,7 @@ class SettingsDefinition:
display_name="BIP-39 passphrase",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=SettingsConstants.ALL_OPTIONS,
selection_options=SettingsConstants.OPTIONS__ENABLED_DISABLED_REQUIRED,
default_value=SettingsConstants.OPTION__ENABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
+2 -4
View File
@@ -3,7 +3,7 @@ from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.settings import SettingsConstants
from seedsigner.views.psbt_views import PSBTSelectSeedView
from seedsigner.views.seed_views import SeedAddPassphrasePromptView, SeedAddPassphraseView
from seedsigner.views.seed_views import SeedAddPassphraseView
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
@@ -37,9 +37,7 @@ class ScanView(View):
self.controller.storage.set_pending_seed(
Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code)
)
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__PROMPT:
return Destination(SeedAddPassphrasePromptView)
elif self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED:
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED:
return Destination(SeedAddPassphraseView)
else:
return Destination(SeedFinalizeView)
+83 -112
View File
@@ -4,6 +4,8 @@ import time
from binascii import hexlify
from embit.networks import NETWORKS
from seedsigner.models.settings_definition import SettingsDefinition
from .view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
@@ -57,6 +59,9 @@ class SeedsMenuView(View):
"""****************************************************************************
Loading seeds, passphrases, etc
****************************************************************************"""
class LoadSeedView(View):
def run(self):
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
@@ -189,9 +194,6 @@ class SeedMnemonicInvalidView(View):
"""****************************************************************************
Loading seeds, passphrases, etc
****************************************************************************"""
class SeedFinalizeView(View):
def __init__(self):
super().__init__()
@@ -200,101 +202,27 @@ class SeedFinalizeView(View):
def run(self):
PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.UNLOCK)
FINALIZE = "Done"
CANCEL = ("Cancel", None, None, "red")
PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.LOCK)
button_data = []
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) in [
SettingsConstants.OPTION__ENABLED,
SettingsConstants.OPTION__PROMPT,
SettingsConstants.OPTION__REQUIRED]:
if self.seed.passphrase:
PASSPHRASE = "Edit Passphrase"
button_data.append(PASSPHRASE)
button_data.append(FINALIZE)
button_data.append(CANCEL)
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
button_data.append(PASSPHRASE)
selected_menu_num = seed_screens.SeedFinalizeScreen(
fingerprint=self.fingerprint,
button_data=button_data,
).display()
if button_data[selected_menu_num] == PASSPHRASE:
return Destination(SeedAddPassphraseView)
elif button_data[selected_menu_num] == FINALIZE:
if button_data[selected_menu_num] == 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] == CANCEL:
return Destination(SeedDiscardView)
class SeedDiscardView(View):
def __init__(self, seed_num: int = None):
super().__init__()
self.seed_num = seed_num
print(f"self.seed_num: {self.seed_num}")
if self.seed_num is not None:
self.seed = self.controller.get_seed(self.seed_num)
else:
self.seed = self.controller.storage.pending_seed
def run(self):
KEEP = "Keep Seed"
DISCARD = ("Discard", None, None, "red")
button_data = [KEEP, DISCARD]
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
selected_menu_num = WarningScreen(
title="Discard Seed?",
warning_headline=None,
warning_text=f"Remove seed {fingerprint} from memory?",
show_back_button=False,
button_data=button_data,
).display()
print(f"selected_menu_num: {selected_menu_num}")
if button_data[selected_menu_num] == KEEP:
if self.seed_num is not None:
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
else:
return Destination(SeedFinalizeView)
elif button_data[selected_menu_num] == DISCARD:
if self.seed_num is not None:
self.controller.discard_seed(self.seed_num)
else:
self.controller.storage.clear_pending_seed()
return Destination(MainMenuView)
class SeedAddPassphrasePromptView(View):
def run(self):
YES = "Yes"
NO = "No"
button_data = [YES, NO]
selected_menu_num = LargeButtonScreen(
title="Add Passphrase?",
button_data=button_data
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if button_data[selected_menu_num] == YES:
elif button_data[selected_menu_num] == PASSPHRASE:
return Destination(SeedAddPassphraseView)
elif button_data[selected_menu_num] == NO:
return Destination(SeedFinalizeView)
class SeedAddPassphraseView(View):
@@ -327,8 +255,8 @@ class SeedReviewPassphraseView(View):
def run(self):
EDIT = "Edit passphrase"
CONTINUE = "Continue"
button_data = [EDIT, CONTINUE]
DONE = "Done"
button_data = [EDIT, DONE]
# Get the before/after fingerprints
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
@@ -348,21 +276,60 @@ class SeedReviewPassphraseView(View):
show_back_button=False,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == EDIT:
if button_data[selected_menu_num] == EDIT:
return Destination(SeedAddPassphraseView)
elif button_data[selected_menu_num] == CONTINUE:
return Destination(SeedFinalizeView)
elif button_data[selected_menu_num] == DONE:
seed_num = self.controller.storage.finalize_pending_seed()
return Destination(SeedOptionsView, view_args={"seed_num": seed_num})
class SeedDiscardView(View):
def __init__(self, seed_num: int = None):
super().__init__()
self.seed_num = seed_num
print(f"self.seed_num: {self.seed_num}")
if self.seed_num is not None:
self.seed = self.controller.get_seed(self.seed_num)
else:
self.seed = self.controller.storage.pending_seed
def run(self):
KEEP = "Keep Seed"
DISCARD = ("Discard", None, None, "red")
button_data = [KEEP, DISCARD]
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
selected_menu_num = WarningScreen(
title="Discard Seed?",
warning_headline=None,
warning_text=f"Wipe seed {fingerprint} from the device?",
show_back_button=False,
button_data=button_data,
).display()
print(f"selected_menu_num: {selected_menu_num}")
if button_data[selected_menu_num] == KEEP:
if self.seed_num is not None:
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
else:
return Destination(SeedFinalizeView)
elif button_data[selected_menu_num] == DISCARD:
if self.seed_num is not None:
self.controller.discard_seed(self.seed_num)
else:
self.controller.storage.clear_pending_seed()
return Destination(MainMenuView)
"""****************************************************************************
Views for actions on individual seeds:
****************************************************************************"""
class SeedOptionsView(View):
def __init__(self, seed_num: int):
super().__init__()
@@ -439,7 +406,7 @@ class SeedWordsWarningView(View):
def run(self):
destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num})
destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0})
if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED:
# Forward straight to showing the words
destination.skip_current_view = True
@@ -459,38 +426,39 @@ class SeedWordsWarningView(View):
class SeedWordsView(View):
def __init__(self, seed_num: int, is_first_page: bool = True):
def __init__(self, seed_num: int, page_index: int = 0):
super().__init__()
self.seed_num = seed_num
self.seed = self.controller.get_seed(self.seed_num)
self.is_first_page = is_first_page
self.page_index = page_index
self.num_pages=int(len(self.seed.mnemonic_list)/4)
def run(self):
NEXT_12 = "Next"
SEED_OPTIONS = "Seed Options"
NEXT = "Next"
DONE = "Done"
button_data = []
if self.is_first_page and len(self.seed.mnemonic_list) == 24:
button_data.append(NEXT_12)
if self.page_index < self.num_pages - 1:
button_data.append(NEXT)
else:
button_data.append(SEED_OPTIONS)
button_data.append(DONE)
selected_menu_num = seed_screens.SeedWordsScreen(
seed=self.seed,
is_first_page=self.is_first_page,
page_index=self.page_index,
num_pages=self.num_pages,
button_data=button_data,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if button_data[selected_menu_num] == NEXT_12:
# Go on to page 2
return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "is_first_page": False})
if button_data[selected_menu_num] == NEXT:
return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1})
elif button_data[selected_menu_num] == SEED_OPTIONS:
# Back to SeedOptions for this seed; cannot back ("<") to this View.
elif button_data[selected_menu_num] == DONE:
# Must clear history to avoid BACK button returning to private info
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True)
@@ -513,7 +481,7 @@ class SeedExportXpubSigTypeView(View):
MULTISIG = "Multisig"
button_data=[SINGLE_SIG, MULTISIG]
selected_menu_num = LargeButtonScreen(
selected_menu_num = ButtonListScreen(
title="Export Xpub",
button_data=button_data
).display()
@@ -543,17 +511,21 @@ class SeedExportXpubScriptTypeView(View):
args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
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)
selected_menu_num = ButtonListScreen(
title="Export Xpub",
is_button_text_centered=False,
is_bottom_list=True,
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES),
button_data=button_data,
).display()
if selected_menu_num < len(SettingsConstants.ALL_SCRIPT_TYPES):
args["script_type"] = SettingsConstants.ALL_SCRIPT_TYPES[selected_menu_num][0]
script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
selected_display_name = button_data[selected_menu_num]
args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
if SettingsConstants.ALL_SCRIPT_TYPES[selected_menu_num][0] == SettingsConstants.CUSTOM_DERIVATION:
if button_data[selected_menu_num] == SettingsConstants.CUSTOM_DERIVATION:
return Destination(SeedExportXpubCustomDerivationView, view_args=args)
return Destination(SeedExportXpubCoordinatorView, view_args=args)
@@ -615,7 +587,6 @@ class SeedExportXpubCoordinatorView(View):
selected_menu_num = ButtonListScreen(
title="Export Xpub",
is_button_text_centered=False,
is_bottom_list=True,
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
).display()
+13 -2
View File
@@ -4,6 +4,7 @@ import time
from PIL import Image
from PIL.ImageOps import autocontrast
from seedsigner.gui.screens.screen import LargeButtonScreen
from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants
@@ -45,6 +46,9 @@ class ToolsMenuView(View):
"""****************************************************************************
Image entropy Views
****************************************************************************"""
class ToolsImageEntropyLivePreviewView(View):
def run(self):
self.controller.image_entropy_preview_frames = None
@@ -99,10 +103,14 @@ class ToolsImageEntropyMnemonicLengthView(View):
TWENTYFOUR_WORDS = "24 words"
button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS]
selected_menu_num = ButtonListScreen(
# selected_menu_num = ButtonListScreen(
# title="Mnemonic Length?",
# button_data=button_data,
# ).display()
selected_menu_num = LargeButtonScreen(
title="Mnemonic Length?",
button_data=button_data,
is_bottom_list=True,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
@@ -164,6 +172,9 @@ class ToolsImageEntropyMnemonicLengthView(View):
"""****************************************************************************
Calc final word Views
****************************************************************************"""
class ToolsCalcFinalWordNumWordsView(View):
def run(self):
TWELVE = "12 words"
+6
View File
@@ -33,6 +33,12 @@ class BackStackView:
As with http requests, Views can receive input vars to inform their behavior. Views
can also prepare the next set of vars to set up the next View that should be
displayed (akin to Flask's `return redirect(url, param1=x, param2=y))`).
Navigation guidance:
"Next" - Continue to next step
"Done" - End of flow, return to entry point (non-destructive)
"OK/Close" - Exit current screen (non-destructive)
"Cancel" - End task and return to entry point (destructive)
"""
class View:
def __init__(self) -> None: