Merge pull request #223 from SeedSigner/dev

0.5.0 to 0.5.1 release
This commit is contained in:
Nick Klockenga
2022-06-17 02:55:32 -04:00
committed by GitHub
17 changed files with 103547 additions and 402 deletions
+8
View File
@@ -142,6 +142,14 @@ The Orange Pill enclosure design offers a more finished look that includes butto
The upper and lower portions of the enclosure can be printed using a standard FDM 3D printer, no supports necessary. The buttons and joystick nub should ideally be produced with a SLA/resin printer. An overview of the entire assembly process can be found [here](https://youtu.be/aIIc2DiZYcI). To access the design files and printable models, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/orange_pill).
### Community Designs
* [Lil Pill](https://cults3d.com/en/3d-model/gadget/lil-pill-seedsigner-case) by @_CyberNomad
* [OrangeSurf Case](https://github.com/orangesurf/orangesurf-seedsigner-case) by @OrangeSurfBTC
* [PS4 Seedsigner](https://www.thingiverse.com/thing:5363525) by @Silexperience
* [OpenPill Faceplate](https://www.printables.com/en/model/179924-seedsigner-open-pill-cover-plates-digital-cross-jo) by @Revetuzo
* [Waveshare CoverPlate](https://cults3d.com/en/3d-model/various/seedsigner-coverplate-for-waveshare-1-3-inch-lcd-hat-with-240x240-pixel-display) by @Adathome1
---------------
# SeedQR Printable Templates
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
embit==0.4.10
embit @ git+https://github.com/seedsigner/embit.git@03cba13e0add64b241972c67e2aa970cb2e3a5d2
numpy==1.21.1
picamera==1.13
Pillow==8.2.0
+1 -1
View File
@@ -48,7 +48,7 @@ class Controller(Singleton):
rather than at the top in order avoid circular imports.
"""
VERSION = "0.5.0"
VERSION = "0.5.1"
# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
+3 -1
View File
@@ -289,7 +289,9 @@ class TextArea(BaseComponent):
# fits in its bounding rect (plus accounting for edge padding) using its given
# font.
# Measure from left baseline ("ls")
# getbbox() seems to ignore "\n" so won't affect height calcs
# TODO: getbbox() seems to ignore "\n" so isn't properly factored into height
# calcs and yields incorrect full_text_width. For now must specify self.height to
# render properly. Centering will be wrong.
(left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls")
self.text_font_height = -1 * top
self.bbox_height = self.text_font_height + bottom
+8 -4
View File
@@ -118,8 +118,12 @@ class PSBTOverviewScreen(ButtonListScreen):
# max_destination_col_width += curve_width
# Now let's maximize the actual destination col by adjusting our addr truncation
def calculate_destination_col_width(truncate_at: int):
def calculate_destination_col_width(truncate_at: int = 0):
def truncate_destination_addr(addr):
# TODO: Properly handle the ellipsis truncation in different languages
if len(addr) <= truncate_at + len("..."):
# No point in truncating
return addr
return f"{addr[:truncate_at]}..."
destination_column = []
@@ -129,7 +133,7 @@ class PSBTOverviewScreen(ButtonListScreen):
destination_column.append(truncate_destination_addr(addr))
for i in range(0, self.num_self_transfer_outputs):
destination_column.append(f"self-transfer")
destination_column.append(truncate_destination_addr("self-transfer"))
else:
# destination_column.append(f"{len(self.destination_addresses)} recipients")
destination_column.append(f"recipient 1")
@@ -151,10 +155,10 @@ class PSBTOverviewScreen(ButtonListScreen):
if len(self.destination_addresses) + self.num_self_transfer_outputs > 3:
# We're not going to display any destination addrs so truncation doesn't matter
(destination_text_width, destination_column) = calculate_destination_col_width(truncate_at=0)
(destination_text_width, destination_column) = calculate_destination_col_width()
else:
# Steadliy widen out the destination column until we run out of space
for i in range(6, 13):
for i in range(6, 14):
(new_width, new_col_text) = calculate_destination_col_width(truncate_at=i)
if new_width > max_destination_col_width:
break
+211 -1
View File
@@ -3,13 +3,14 @@ import time
from dataclasses import dataclass
from PIL import Image, ImageDraw, ImageColor
from typing import Any, List, Tuple
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.settings import Settings, SettingsConstants
from ..components import (GUIConstants, BaseComponent, Button, Icon, LargeIconButton, SeedSignerCustomIconConstants, TopNav,
from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav,
TextArea, load_image)
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
@@ -871,3 +872,212 @@ class PowerOffScreen(BaseTopNavScreen):
screen_y=self.top_nav.height,
height=self.canvas_height - self.top_nav.height,
))
@dataclass
class KeyboardScreen(BaseTopNavScreen):
"""
Generalized Screen for a single Keyboard layout writing user input to a
TextEntryDisplay.
Args:
* rows
* cols
* keyboard_font_name
* keyboard_font_size: Specify `None` to auto-size to Key height.
* key_height: Specify `None` to maximize key height to available space.
* keys_charset: Specify the chars displayed on the keys of the keyboard.
* keys_to_values: Optional mapping from key_charset to input value (e.g. dice icon to digit).
* return_after_n_chars: exits and returns the user's input after n characters.
* show_save_button: Render a KEY3 soft button for save & exit
* initial_value: initialize the TextEntryDisplay with an existing string
"""
rows: int = None
cols: int = None
keyboard_font_name: str = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
keyboard_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2
key_height: int = None
keys_charset: str = None
keys_to_values: dict = None
return_after_n_chars: int = None
show_save_button: bool = False
initial_value: str = ""
def __post_init__(self):
super().__post_init__()
if self.initial_value:
self.user_input = self.initial_value
else:
self.user_input = ""
# Set up the keyboard params
if self.show_save_button:
right_panel_buttons_width = 60
hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING
hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60
self.keyboard_width = self.canvas_width - (GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING)
# Render the right button panel (only has a Key3 "Save" button)
self.save_button = IconButton(
icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK,
icon_color=GUIConstants.SUCCESS_COLOR,
width=right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y,
)
self.components.append(self.save_button)
else:
self.keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING
text_entry_display_y = self.top_nav.height
text_entry_display_height = 30
keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING
if self.key_height is None:
self.key_height = int((self.canvas_height - GUIConstants.EDGE_PADDING - text_entry_display_y - text_entry_display_height - GUIConstants.COMPONENT_PADDING - (self.rows - 1) * 2) / self.rows)
if self.keyboard_font_size:
font_size = self.keyboard_font_size
else:
# Scale with button height
font_size = self.key_height - GUIConstants.COMPONENT_PADDING
self.keyboard = Keyboard(
draw=self.renderer.draw,
charset=self.keys_charset,
font_name=self.keyboard_font_name,
font_size=font_size,
rows=self.rows,
cols=self.cols,
rect=(
GUIConstants.EDGE_PADDING,
keyboard_start_y,
GUIConstants.EDGE_PADDING + self.keyboard_width,
keyboard_start_y + self.rows * self.key_height + (self.rows - 1) * 2
),
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard.set_selected_key(selected_letter=self.keys_charset[0])
self.text_entry_display = TextEntryDisplay(
canvas=self.renderer.canvas,
rect=(
GUIConstants.EDGE_PADDING,
text_entry_display_y,
self.canvas_width - GUIConstants.EDGE_PADDING,
text_entry_display_y + text_entry_display_height
),
cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR,
is_centered=False,
cur_text=self.initial_value,
)
def _render(self):
super()._render()
self.keyboard.render_keys()
self.text_entry_display.render()
self.renderer.show_image()
def _run(self):
self.cursor_position = len(self.user_input)
# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)
# Check possible exit conditions
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
return RET_CODE__BACK_BUTTON
elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
# Save!
# First show the save button reacting to the click
self.save_button.is_selected = True
self.save_button.render()
self.renderer.show_image()
# Then return the input to the View
if len(self.user_input) > 0:
return self.user_input.strip()
# Process normal input
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
# We're navigating off the previous button
self.top_nav.is_selected = False
self.top_nav.render_buttons()
# Override the actual input w/an ENTER signal for the Keyboard
if input == HardwareButtonsConstants.KEY_DOWN:
input = Keyboard.ENTER_TOP
else:
input = Keyboard.ENTER_BOTTOM
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
# ignore
continue
ret_val = self.keyboard.update_from_input(input)
# Now process the result from the keyboard
if ret_val in Keyboard.EXIT_DIRECTIONS:
self.top_nav.is_selected = True
self.top_nav.render_buttons()
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if len(self.user_input) > 0:
self.user_input = self.user_input[:-1]
self.cursor_position -= 1
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
# User has locked in the current letter
if self.keys_to_values:
# Map the Key display char to its output value (e.g. dice icon to digit)
ret_val = self.keys_to_values[ret_val]
self.user_input += ret_val
self.cursor_position += 1
if self.cursor_position == self.return_after_n_chars:
return self.user_input
# Render a new TextArea over the TopNav title bar
if self.update_title():
TextArea(
text=self.title,
font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME,
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
height=self.top_nav.height,
).render()
self.top_nav.render_buttons()
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
# Live joystick movement; haven't locked this new letter in yet.
# Leave current spot blank for now. Only update the active keyboard keys
# when a selection has been locked in (KEY_PRESS) or removed ("del").
pass
# Render the text entry display and cursor block
self.text_entry_display.render(self.user_input)
self.renderer.show_image()
def update_title(self) -> bool:
"""
Optionally update the self.title after each completed key input.
e.g. to increment the dice roll count:
self.title = f"Roll {self.cursor_position + 1}"
"""
return False
+15 -151
View File
@@ -13,7 +13,7 @@ from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, WarningEdgesMixin
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin
from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants,
calc_text_centering)
@@ -451,7 +451,7 @@ class SeedOptionsScreen(ButtonListScreen):
@dataclass
class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
seed: Seed = None
words: List[str] = None
page_index: int = 0
num_pages: int = 3
is_bottom_list: bool = True
@@ -459,15 +459,9 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
def __post_init__(self):
self.title = f"Seed Words: {self.page_index+1}/{self.num_pages}"
super().__post_init__()
# Can only render 4 words per screen
words_per_page = 4
mnemonic = self.seed.mnemonic_display_list
# 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]
words_per_page = len(self.words)
self.body_x = 0
self.body_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING / 2)
@@ -479,7 +473,7 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
# Calc horizontal center based on longest word
max_word_width = 0
for word in self.mnemonic:
for word in self.words:
(left, top, right, bottom) = font.getbbox(word, anchor="ls")
if right > max_word_width:
max_word_width = right
@@ -503,7 +497,7 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
)
draw = ImageDraw.Draw(self.body_img)
for index, word in enumerate(self.mnemonic):
for index, word in enumerate(self.words):
draw.rounded_rectangle(
(number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height),
fill="#202020",
@@ -553,149 +547,19 @@ class SeedWordsBackupTestPromptScreen(ButtonListScreen):
@dataclass
class SeedExportXpubCustomDerivationScreen(BaseTopNavScreen):
title: str = "Derivation Path"
derivation_path: str = "m/"
class SeedExportXpubCustomDerivationScreen(KeyboardScreen):
def __post_init__(self):
self.title = "Derivation Path"
self.user_input = "m/"
# Specify the keys in the keyboard
self.rows = 3
self.cols = 6
self.keys_charset = "/'0123456789"
self.show_save_button = True
super().__post_init__()
# Set up the keyboard params
right_panel_buttons_width = 60
hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING
hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60
keyboard_width = self.canvas_width - (GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING)
text_entry_display_y = self.top_nav.height
text_entry_display_height = 30
keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING
rows = 3
self.keyboard_digits = Keyboard(
draw=self.renderer.draw,
charset="/'0123456789",
rows=rows,
cols=6,
rect=(
GUIConstants.EDGE_PADDING,
keyboard_start_y,
GUIConstants.EDGE_PADDING + keyboard_width,
keyboard_start_y + rows * GUIConstants.BUTTON_HEIGHT + (rows - 1) * 2
),
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard_digits.set_selected_key(selected_letter="0")
self.text_entry_display = TextEntryDisplay(
canvas=self.renderer.canvas,
rect=(
GUIConstants.EDGE_PADDING,
text_entry_display_y,
self.canvas_width - GUIConstants.EDGE_PADDING,
text_entry_display_y + text_entry_display_height
),
cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR,
is_centered=False,
cur_text=''.join(self.derivation_path)
)
# Render the right button panel (only has a Key3 "Save" button)
self.exit_button = IconButton(
icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK,
icon_color=GUIConstants.SUCCESS_COLOR,
width=right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y,
)
def _render(self):
super()._render()
self.keyboard_digits.render_keys()
self.exit_button.render()
self.text_entry_display.render(self.derivation_path)
self.renderer.show_image()
def _run(self):
cursor_position = len(self.derivation_path)
# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)
# Check our two possible exit conditions
if input == HardwareButtonsConstants.KEY3:
# Save!
self.exit_button.is_selected = True
self.exit_button.render()
self.renderer.show_image()
if len(self.derivation_path) > 0:
return self.derivation_path.strip()
elif self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
# Prev button clicked; return empty string to signal cancel.
return self.top_nav.selected_button
# Process normal input
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
# We're navigating off the previous button
self.top_nav.is_selected = False
self.top_nav.render_buttons()
# Override the actual input w/an ENTER signal for the Keyboard
if input == HardwareButtonsConstants.KEY_DOWN:
input = Keyboard.ENTER_TOP
else:
input = Keyboard.ENTER_BOTTOM
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
# ignore
continue
ret_val = self.keyboard_digits.update_from_input(input)
# Now process the result from the keyboard
if ret_val in Keyboard.EXIT_DIRECTIONS:
self.top_nav.is_selected = True
self.top_nav.render_buttons()
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if len(self.derivation_path) <= 2:
pass
elif cursor_position == len(self.derivation_path):
self.derivation_path = self.derivation_path[:-1]
cursor_position -= 1
else:
self.derivation_path = self.derivation_path[:cursor_position - 1] + self.derivation_path[cursor_position:]
cursor_position -= 1
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
# User has locked in the current letter
if cursor_position == len(self.derivation_path):
self.derivation_path += ret_val
else:
self.derivation_path = self.derivation_path[:cursor_position] + ret_val + self.derivation_path[cursor_position:]
cursor_position += 1
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
# Live joystick movement; haven't locked this new letter in yet.
# Leave current spot blank for now. Only update the active keyboard keys
# when a selection has been locked in (KEY_PRESS) or removed ("del").
pass
# Render the text entry display and cursor block
self.text_entry_display.render(self.derivation_path)
self.renderer.show_image()
@dataclass
+222 -145
View File
@@ -4,7 +4,7 @@ from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen
from seedsigner.hardware.buttons import HardwareButtonsConstants
@@ -111,158 +111,235 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
@dataclass
class ToolsDiceEntropyEntryScreen(BaseTopNavScreen):
total_rolls: int = None
class ToolsDiceEntropyEntryScreen(KeyboardScreen):
def __post_init__(self):
self.title = f"Dice Roll 1/{self.total_rolls}"
# Override values set by the parent class
self.title = f"Dice Roll 1/{self.return_after_n_chars}"
# Specify the keys in the keyboard
self.rows = 3
self.cols = 3
self.keyboard_font_name = GUIConstants.ICON_FONT_NAME__FONT_AWESOME
self.keyboard_font_size = None # Force auto-scaling to Key height
self.keys_charset = "".join([
FontAwesomeIconConstants.DICE_ONE,
FontAwesomeIconConstants.DICE_TWO,
FontAwesomeIconConstants.DICE_THREE,
FontAwesomeIconConstants.DICE_FOUR,
FontAwesomeIconConstants.DICE_FIVE,
FontAwesomeIconConstants.DICE_SIX,
])
# Map Key display chars to actual output values
self.keys_to_values = {
FontAwesomeIconConstants.DICE_ONE: "1",
FontAwesomeIconConstants.DICE_TWO: "2",
FontAwesomeIconConstants.DICE_THREE: "3",
FontAwesomeIconConstants.DICE_FOUR: "4",
FontAwesomeIconConstants.DICE_FIVE: "5",
FontAwesomeIconConstants.DICE_SIX: "6",
}
# Now initialize the parent class
super().__post_init__()
self.dice_rolls = ""
# Set up the keyboard params
keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING
text_entry_display_y = self.top_nav.height
text_entry_display_height = 30
keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING
rows = 3
button_height = int((self.canvas_height - GUIConstants.EDGE_PADDING - text_entry_display_y - text_entry_display_height - GUIConstants.COMPONENT_PADDING - (rows - 1) * 2) / rows)
self.keyboard_digits = Keyboard(
draw=self.renderer.draw,
charset="".join([
FontAwesomeIconConstants.DICE_ONE,
FontAwesomeIconConstants.DICE_TWO,
FontAwesomeIconConstants.DICE_THREE,
FontAwesomeIconConstants.DICE_FOUR,
FontAwesomeIconConstants.DICE_FIVE,
FontAwesomeIconConstants.DICE_SIX,
]),
font_name=GUIConstants.ICON_FONT_NAME__FONT_AWESOME,
font_size=button_height - GUIConstants.COMPONENT_PADDING,
rows=rows,
cols=3,
rect=(
GUIConstants.EDGE_PADDING,
keyboard_start_y,
GUIConstants.EDGE_PADDING + keyboard_width,
keyboard_start_y + rows * button_height + (rows - 1) * 2
),
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard_digits.set_selected_key(selected_letter=FontAwesomeIconConstants.DICE_ONE)
self.text_entry_display = TextEntryDisplay(
canvas=self.renderer.canvas,
rect=(
GUIConstants.EDGE_PADDING,
text_entry_display_y,
self.canvas_width - GUIConstants.EDGE_PADDING,
text_entry_display_y + text_entry_display_height
),
cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR,
is_centered=False,
)
def _render(self):
super()._render()
self.keyboard_digits.render_keys()
self.text_entry_display.render()
self.renderer.show_image()
def _run(self):
cursor_position = len(self.dice_rolls)
# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)
# Check possible exit condition
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
return RET_CODE__BACK_BUTTON
# Process normal input
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
# We're navigating off the previous button
self.top_nav.is_selected = False
self.top_nav.render_buttons()
# Override the actual input w/an ENTER signal for the Keyboard
if input == HardwareButtonsConstants.KEY_DOWN:
input = Keyboard.ENTER_TOP
else:
input = Keyboard.ENTER_BOTTOM
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
# ignore
continue
ret_val = self.keyboard_digits.update_from_input(input)
# Now process the result from the keyboard
if ret_val in Keyboard.EXIT_DIRECTIONS:
self.top_nav.is_selected = True
self.top_nav.render_buttons()
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if len(self.dice_rolls) > 0:
self.dice_rolls = self.dice_rolls[:-1]
cursor_position -= 1
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
# User has locked in the current letter
if ret_val == FontAwesomeIconConstants.DICE_ONE:
ret_val = "1"
elif ret_val == FontAwesomeIconConstants.DICE_TWO:
ret_val = "2"
elif ret_val == FontAwesomeIconConstants.DICE_THREE:
ret_val = "3"
elif ret_val == FontAwesomeIconConstants.DICE_FOUR:
ret_val = "4"
elif ret_val == FontAwesomeIconConstants.DICE_FIVE:
ret_val = "5"
elif ret_val == FontAwesomeIconConstants.DICE_SIX:
ret_val = "6"
self.dice_rolls += ret_val
cursor_position += 1
if cursor_position == self.total_rolls:
return self.dice_rolls
# Render a new TextArea over the TopNav title bar
TextArea(
text=f"Dice Roll {cursor_position + 1}/{self.total_rolls}",
font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME,
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
height=self.top_nav.height,
).render()
self.top_nav.render_buttons()
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
# Live joystick movement; haven't locked this new letter in yet.
# Leave current spot blank for now. Only update the active keyboard keys
# when a selection has been locked in (KEY_PRESS) or removed ("del").
pass
# Render the text entry display and cursor block
self.text_entry_display.render(self.dice_rolls)
self.renderer.show_image()
def update_title(self) -> bool:
self.title = f"Dice Roll {self.cursor_position + 1}/{self.return_after_n_chars}"
return True
@dataclass
class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen):
class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
mnemonic_length: int = None
num_entropy_bits: int = None
def __post_init__(self):
self.title = "Build Final Word"
self.is_bottom_list = True
self.is_button_text_centered = True
super().__post_init__()
self.components.append(TextArea(
text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.",
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))
@dataclass
class ToolsCoinFlipEntryScreen(KeyboardScreen):
def __post_init__(self):
# Override values set by the parent class
self.title = f"Coin Flip 1/{self.return_after_n_chars}"
# Specify the keys in the keyboard
self.rows = 1
self.cols = 4
self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING
self.keys_charset = "10"
# Now initialize the parent class
super().__post_init__()
self.components.append(TextArea(
text="Heads = 1",
screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING,
))
self.components.append(TextArea(
text="Tails = 0",
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
))
def update_title(self) -> bool:
self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}"
return True
@dataclass
class ToolsCalcFinalWordScreen(ButtonListScreen):
selected_final_word: str = None
selected_final_bits: str = None
checksum_bits: str = None
actual_final_word: str = None
def __post_init__(self):
self.is_bottom_list = True
super().__post_init__()
# First what's the total bit display width and where do the checksum bits start?
bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
bit_display_x = int((self.canvas_width - bit_display_width)/2)
checksum_x += bit_display_x
# Display the user's additional entropy input
if self.selected_final_word:
selection_text = self.selected_final_word
keeper_selected_bits = self.selected_final_bits[:11 - len(self.checksum_bits)]
# The word's least significant bits will be rendered differently to convey
# the fact that they're being discarded.
discard_selected_bits = self.selected_final_bits[-1*len(self.checksum_bits):]
else:
# User entered coin flips or all zeros
selection_text = self.selected_final_bits
keeper_selected_bits = self.selected_final_bits
# We'll append spacer chars to preserve the vertical alignment (most
# significant n bits always rendered in same column)
discard_selected_bits = "_" * (len(self.checksum_bits))
self.components.append(TextArea(
text=f"""Your input: \"{selection_text}\"""",
screen_y=self.top_nav.height,
))
# ...and that entropy's associated 11 bits
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
self.components.append(TextArea(
text=keeper_selected_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))
# Render the least significant bits that will be replaced by the checksum in a
# de-emphasized font color.
self.components.append(TextArea(
text=discard_selected_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.LABEL_FONT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))
# Show the checksum..
self.components.append(TextArea(
text="Checksum",
edge_padding=0,
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
))
# ...and its actual bits. Prepend spacers to keep vertical alignment
checksum_spacer = "_" * (11 - len(self.checksum_bits))
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
# This time we de-emphasize the prepended spacers that are irrelevant
self.components.append(TextArea(
text=checksum_spacer,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.LABEL_FONT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))
# And especially highlight (orange!) the actual checksum bits
self.components.append(TextArea(
text=self.checksum_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
font_color=GUIConstants.ACCENT_COLOR,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
is_text_centered=False,
))
# And now the *actual* final word after merging the bit data
self.components.append(TextArea(
text=f"""Final Word: \"{self.actual_final_word}\"""",
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
))
# Once again show the bits that came from the user's entropy...
num_checksum_bits = len(self.checksum_bits)
user_component = self.selected_final_bits[:11 - num_checksum_bits]
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
self.components.append(TextArea(
text=user_component,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
is_text_centered=False,
))
# ...and append the checksum's bits, still highlighted in orange
self.components.append(TextArea(
text=self.checksum_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.ACCENT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
is_text_centered=False,
))
@dataclass
class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
final_word: str = None
mnemonic_word_length: int = 12
fingerprint: str = None
+15 -13
View File
@@ -8,23 +8,25 @@ from typing import List
from seedsigner.models.seed import Seed
def calculate_checksum(partial_mnemonic: list, wordlist_language_code: str) -> List[str]:
""" Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """
if len(partial_mnemonic) not in [11, 23]:
raise Exception("Pass in a 11- or 23-word mnemonic")
def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]:
"""
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
If 11- or 23-words are provided, append word `0000` to end of list as temp final
word.
"""
if len(mnemonic) in [11, 23]:
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0])
if len(mnemonic) not in [12, 24]:
raise Exception("Pass in a 12- or 24-word mnemonic")
# Work on a copy of the input list
mnemonic_copy = partial_mnemonic.copy()
# 12-word seeds contribute 7 bits of entropy to the final word; 24-word seeds
# contribute 3 bits. But we don't have any partial entropy bits to use to help us
# create the final word. So just default to filling those missing values with zeroes
# ("abandon" is word 0000, so effectively inserts zeroes).
mnemonic_copy.append("abandon")
mnemonic_copy = mnemonic.copy()
# Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation
# because we have to assume it's incorrect since we just hard-coded it above; we'll
# fix that next.
# because we assume it's incorrect since we either let the user select their own
# final word OR we injected the 0000 word from the wordlist.
mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code))
# This function will convert the bytes back into a mnemonic, but it will also
+33 -19
View File
@@ -32,7 +32,8 @@ class PSBTSelectSeedView(View):
seeds = self.controller.storage.seeds
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
ENTER_WORDS = "Enter 12/24 words"
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))
@@ -45,7 +46,8 @@ class PSBTSelectSeedView(View):
pass
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
button_data.append(SCAN_SEED)
button_data.append(ENTER_WORDS)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)
selected_menu_num = ButtonListScreen(
title="Select Signer",
@@ -68,8 +70,12 @@ class PSBTSelectSeedView(View):
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == ENTER_WORDS:
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else:
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
@@ -78,18 +84,22 @@ class PSBTOverviewView(View):
def __init__(self):
super().__init__()
# The PSBTParser takes a while to read the PSBT. Run the loading screen while we
# wait.
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
self.loading_screen.start()
self.loading_screen = None
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
# Must run the PSBTParser or re-parse
self.controller.psbt_parser = PSBTParser(
self.controller.psbt,
seed=self.controller.psbt_seed,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)
)
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
# we wait.
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
self.loading_screen.start()
try:
self.controller.psbt_parser = PSBTParser(
self.controller.psbt,
seed=self.controller.psbt_seed,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)
)
except Exception as e:
self.loading_screen.stop()
raise e
def run(self):
@@ -126,7 +136,8 @@ class PSBTOverviewView(View):
)
# Everything is set. Stop the loading screen
self.loading_screen.stop()
if self.loading_screen:
self.loading_screen.stop()
# Run the overview screen
selected_menu_num = screen.display()
@@ -145,6 +156,8 @@ class PSBTOverviewView(View):
else:
return Destination(PSBTMathView)
class PSBTUnsupportedScriptTypeWarningView(View):
def run(self):
selected_menu_num = WarningScreen(
@@ -462,11 +475,12 @@ class PSBTFinalizeView(View):
loading_screen = LoadingScreenThread(text="Signing PSBT...")
loading_screen.start()
sig_cnt = PSBTParser.sig_count(psbt)
psbt.sign_with(psbt_parser.root)
trimmed_psbt = PSBTParser.trim(psbt)
loading_screen.stop()
try:
sig_cnt = PSBTParser.sig_count(psbt)
psbt.sign_with(psbt_parser.root)
trimmed_psbt = PSBTParser.trim(psbt)
finally:
loading_screen.stop()
if sig_cnt == PSBTParser.sig_count(trimmed_psbt):
# Signing failed / didn't do anything
+78 -61
View File
@@ -73,13 +73,13 @@ class SeedsMenuView(View):
class LoadSeedView(View):
def run(self):
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
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_24WORD,
TYPE_12WORD,
TYPE_24WORD,
CREATE,
]
@@ -96,14 +96,14 @@ class LoadSeedView(View):
from .scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == TYPE_24WORD:
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == TYPE_24WORD:
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == CREATE:
from .tools_views import ToolsMenuView
return Destination(ToolsMenuView)
@@ -127,13 +127,14 @@ class SeedMnemonicEntryView(View):
if ret == RET_CODE__BACK_BUTTON:
if self.cur_word_index > 0:
return Destination(
SeedMnemonicEntryView,
view_args={
"cur_word_index": self.cur_word_index - 1,
"is_calc_final_word": self.is_calc_final_word
}
)
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)
@@ -142,17 +143,15 @@ class SeedMnemonicEntryView(View):
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2:
# Time to calculate the last word
# TODO: Option to add missing entropy for the last word:
# * 3 bits for a 24-word seed
# * 7 bits for a 12-word seed
from seedsigner.helpers import mnemonic_generation
# Time to calculate the last word. User must decide how they want to specify
# the last bits of entropy for the final word.
from seedsigner.views.tools_views import ToolsCalcFinalWordFinalizePromptView
return Destination(ToolsCalcFinalWordFinalizePromptView)
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1:
# Time to calculate the last word. User must either select a final word to
# contribute entropy to the checksum word OR we assume 0 ("abandon").
from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView
full_mnemonic = mnemonic_generation.calculate_checksum(
self.controller.storage.pending_mnemonic[:-1], # Must omit the last word's empty value
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
)
self.controller.storage.update_pending_mnemonic(full_mnemonic[-1], self.cur_word_index+1)
return Destination(ToolsCalcFinalWordShowFinalWordView)
if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1:
@@ -529,7 +528,7 @@ class SeedExportXpubCustomDerivationView(View):
def run(self):
ret = seed_screens.SeedExportXpubCustomDerivationScreen(
derivation_path=self.custom_derivation_path
initial_value=self.custom_derivation_path,
).display()
if ret == RET_CODE__BACK_BUTTON:
@@ -648,46 +647,50 @@ class SeedExportXpubDetailsView(View):
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
self.loading_screen.start()
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
else:
derivation_path = PSBTParser.calc_derivation(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
wallet_type=self.sig_type,
script_type=self.script_type
)
try:
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
else:
derivation_path = PSBTParser.calc_derivation(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
wallet_type=self.sig_type,
script_type=self.script_type
)
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
version = embit.bip32.detect_version(
derivation_path,
default="xpub",
network=embit_network
)
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
version = embit.bip32.detect_version(
derivation_path,
default="xpub",
network=embit_network
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed_bytes,
version=embit_network["xprv"]
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed_bytes,
version=embit_network["xprv"]
)
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation_path)
xpub = xprv.to_public()
xpub_base58 = xpub.to_string(version=version)
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation_path)
xpub = xprv.to_public()
xpub_base58 = xpub.to_string(version=version)
screen = seed_screens.SeedExportXpubDetailsScreen(
fingerprint=fingerprint,
has_passphrase=self.seed.passphrase is not None,
derivation_path=derivation_path,
xpub=xpub_base58,
)
screen = seed_screens.SeedExportXpubDetailsScreen(
fingerprint=fingerprint,
has_passphrase=self.seed.passphrase is not None,
derivation_path=derivation_path,
xpub=xpub_base58,
)
self.loading_screen.stop()
selected_menu_num = screen.display()
else:
selected_menu_num = 0
finally:
self.loading_screen.stop()
selected_menu_num = screen.display()
else:
self.loading_screen.stop()
selected_menu_num = 0
if selected_menu_num == 0:
return Destination(
@@ -793,6 +796,12 @@ class SeedWordsView(View):
NEXT = "Next"
DONE = "Done"
# Slice the mnemonic to our current 4-word section
words_per_page = 4 # TODO: eventually make this configurable for bigger screens?
mnemonic = self.seed.mnemonic_display_list
words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page]
button_data = []
if self.page_index < self.num_pages - 1 or self.seed_num is None:
button_data.append(NEXT)
@@ -800,7 +809,8 @@ class SeedWordsView(View):
button_data.append(DONE)
selected_menu_num = seed_screens.SeedWordsScreen(
seed=self.seed,
title=f"Seed Words: {self.page_index+1}/{self.num_pages}",
words=words,
page_index=self.page_index,
num_pages=self.num_pages,
button_data=button_data,
@@ -1314,7 +1324,8 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
seeds = self.controller.storage.seeds
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
ENTER_WORDS = "Enter 12/24 words"
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
text = "Load the seed to verify"
@@ -1330,7 +1341,8 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
text = "Select seed to verify"
button_data.append(SCAN_SEED)
button_data.append(ENTER_WORDS)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)
selected_menu_num = seed_screens.SeedSingleSigAddressVerificationSelectSeedScreen(
title="Verify Address",
@@ -1357,7 +1369,12 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] == ENTER_WORDS:
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else:
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
+138 -5
View File
@@ -8,7 +8,7 @@ from PIL.ImageOps import autocontrast
from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen
from seedsigner.helpers import mnemonic_generation
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants
@@ -200,7 +200,7 @@ class ToolsDiceEntropyEntryView(View):
def run(self):
ret = ToolsDiceEntropyEntryScreen(
total_rolls=self.total_rolls,
return_after_n_chars=self.total_rolls,
).display()
if ret == RET_CODE__BACK_BUTTON:
@@ -240,15 +240,148 @@ class ToolsCalcFinalWordNumWordsView(View):
elif button_data[selected_menu_num] == TWELVE:
self.controller.storage.init_pending_mnemonic(12)
return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True})
# 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:
self.controller.storage.init_pending_mnemonic(24)
return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True})
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
class ToolsCalcFinalWordFinalizePromptView(View):
def run(self):
mnemonic = self.controller.storage.pending_mnemonic
mnemonic_length = len(mnemonic)
if mnemonic_length == 12:
num_entropy_bits = 7
else:
num_entropy_bits = 3
COIN_FLIPS = "Coin flip entropy"
SELECT_WORD = f"Word selection entropy"
ZEROS = "Finalize with zeros"
button_data = [COIN_FLIPS, SELECT_WORD, ZEROS]
selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen(
mnemonic_length=mnemonic_length,
num_entropy_bits=num_entropy_bits,
button_data=button_data,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == COIN_FLIPS:
return Destination(ToolsCalcFinalWordCoinFlipsView)
elif button_data[selected_menu_num] == SELECT_WORD:
# Clear the final word slot, just in case we're returning via BACK button
self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1)
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1))
elif button_data[selected_menu_num] == ZEROS:
# User skipped the option to select a final word to provide last bits of
# entropy. We'll insert all zeros and piggy-back on the coin flip attr
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.controller.storage.update_pending_mnemonic(Seed.get_wordlist(wordlist_language_code)[0], mnemonic_length - 1)
return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips="0" * num_entropy_bits))
class ToolsCalcFinalWordCoinFlipsView(View):
def run(self):
mnemonic = self.controller.storage.pending_mnemonic
mnemonic_length = len(mnemonic)
if mnemonic_length == 12:
total_flips = 7
else:
total_flips = 3
ret_val = ToolsCoinFlipEntryScreen(
return_after_n_chars=total_flips,
).display()
if ret_val == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
else:
print(ret_val)
binary_string = ret_val + "0" * (11 - total_flips)
wordlist_index = int(binary_string, 2)
wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
word = wordlist[wordlist_index]
self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1)
return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val))
class ToolsCalcFinalWordShowFinalWordView(View):
def __init__(self, coin_flips=None):
super().__init__()
self.coin_flips = coin_flips
def run(self):
# Construct the actual final word. The user's selected_final_word
# contributes:
# * 3 bits to a 24-word seed (plus 8-bit checksum)
# * 7 bits to a 12-word seed (plus 4-bit checksum)
from seedsigner.helpers import mnemonic_generation
mnemonic = self.controller.storage.pending_mnemonic
mnemonic_length = len(mnemonic)
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
wordlist = Seed.get_wordlist(wordlist_language_code)
final_mnemonic = mnemonic_generation.calculate_checksum(
mnemonic=self.controller.storage.pending_mnemonic,
wordlist_language_code=wordlist_language_code,
)
self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1)
# Prep the user's selected word (if there was one) and the actual final word for
# the display.
if self.coin_flips:
selected_final_word = None
selected_final_bits = self.coin_flips
else:
# Convert the user's final word selection into its binary index equivalent
selected_final_word = mnemonic[-1]
selected_final_bits = format(wordlist.index(selected_final_word), '011b')
# And grab the actual final word's checksum bits
actual_final_word = self.controller.storage.pending_mnemonic[-1]
if mnemonic_length == 12:
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:]
else:
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:]
NEXT = "Next"
button_data = [NEXT]
selected_menu_num = ToolsCalcFinalWordScreen(
title="Final Word Calc",
button_data=button_data,
selected_final_word=selected_final_word,
selected_final_bits=selected_final_bits,
checksum_bits=checksum_bits,
actual_final_word=actual_final_word,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] == NEXT:
return Destination(ToolsCalcFinalWordDoneView)
class ToolsCalcFinalWordDoneView(View):
def run(self):
mnemonic = self.controller.storage.pending_mnemonic
mnemonic_word_length = len(mnemonic)
@@ -258,7 +391,7 @@ class ToolsCalcFinalWordShowFinalWordView(View):
DISCARD = ("Discard", None, None, "red")
button_data = [LOAD, DISCARD]
selected_menu_num = ToolsCalcFinalWordShowFinalWordScreen(
selected_menu_num = ToolsCalcFinalWordDoneScreen(
final_word=final_word,
mnemonic_word_length=mnemonic_word_length,
fingerprint=self.controller.storage.get_pending_mnemonic_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
+20
View File
@@ -43,6 +43,26 @@ def test_calculate_checksum():
def test_calculate_checksum_with_default_final_word():
""" 11-word and 23-word mnemonics use word `0000` as a temp final word to complete
the mnemonic.
"""
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert mnemonic1 == mnemonic2
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert mnemonic1 == mnemonic2
def test_verify_against_coldcard_sample():
""" https://coldcard.com/docs/verifying-dice-roll-math """
dice_rolls = "123456"