mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user