mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 15:38:25 +00:00
Merge branch 'dev' into initial_multilanguage
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
Babel==2.10.1
|
||||
embit==0.4.10
|
||||
embit @ git+https://github.com/seedsigner/embit.git@03cba13e0add64b241972c67e2aa970cb2e3a5d2
|
||||
numpy==1.21.1
|
||||
picamera==1.13
|
||||
Pillow==9.1.0
|
||||
|
||||
@@ -49,7 +49,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.
|
||||
|
||||
@@ -382,7 +382,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
|
||||
|
||||
@@ -121,10 +121,14 @@ 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):
|
||||
# TRANSLATOR_NOTE: Display a truncated address (e.g. "bc1qabc...")
|
||||
return _("{}...").format(addr[:truncate_at])
|
||||
# TRANSLATOR_NOTE: Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...")
|
||||
if len(addr) <= truncate_at + len(_("...")):
|
||||
# No point in truncating
|
||||
return addr
|
||||
|
||||
return addr[:truncate_at] + _("...")
|
||||
|
||||
destination_column = []
|
||||
|
||||
@@ -133,7 +137,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
destination_column.append(truncate_destination_addr(addr))
|
||||
|
||||
for i in range(0, self.num_self_transfer_outputs):
|
||||
destination_column.append(_("self-transfer"))
|
||||
destination_column.append(truncate_destination_addr(_("self-transfer")))
|
||||
else:
|
||||
# destination_column.append(f"{len(self.destination_addresses)} recipients")
|
||||
destination_column.append(_("recipient 1"))
|
||||
@@ -158,10 +162,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
|
||||
|
||||
@@ -152,7 +152,195 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@@ -34,7 +34,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))
|
||||
@@ -47,7 +48,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"),
|
||||
@@ -70,8 +72,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)
|
||||
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ class LoadSeedView(View):
|
||||
CREATE = (_("Create a seed"), FontAwesomeIconConstants.PLUS)
|
||||
button_data=[
|
||||
SEED_QR,
|
||||
TYPE_24WORD,
|
||||
TYPE_12WORD,
|
||||
TYPE_24WORD,
|
||||
CREATE,
|
||||
]
|
||||
|
||||
@@ -98,14 +98,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)
|
||||
@@ -130,13 +130,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)
|
||||
@@ -145,17 +146,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:
|
||||
@@ -679,60 +678,48 @@ class SeedExportXpubDetailsView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
# The calc_derivation takes a few moments. Run the loading screen while we wait.
|
||||
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)
|
||||
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
|
||||
fingerprint=fingerprint,
|
||||
has_passphrase=self.seed.passphrase is not None,
|
||||
derivation_path=derivation_path,
|
||||
xpub=xpub_base58,
|
||||
).display()
|
||||
|
||||
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()
|
||||
else:
|
||||
selected_menu_num = 0
|
||||
|
||||
if selected_menu_num == 0:
|
||||
return Destination(
|
||||
SeedExportXpubQRDisplayView,
|
||||
{
|
||||
"seed_num": self.seed_num,
|
||||
"sig_type": self.sig_type,
|
||||
"script_type": self.script_type,
|
||||
"coordinator": self.coordinator,
|
||||
"derivation_path": derivation_path,
|
||||
}
|
||||
@@ -744,7 +731,7 @@ class SeedExportXpubDetailsView(View):
|
||||
|
||||
|
||||
class SeedExportXpubQRDisplayView(View):
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, derivation_path: str):
|
||||
def __init__(self, seed_num: int, coordinator: str, derivation_path: str):
|
||||
super().__init__()
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
|
||||
@@ -1363,7 +1350,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")
|
||||
@@ -1379,7 +1367,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"),
|
||||
@@ -1406,7 +1395,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)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,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
|
||||
@@ -241,15 +241,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)
|
||||
@@ -259,7 +392,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