Merge branch 'dev' into initial_multilanguage

This commit is contained in:
kdmukai
2022-06-27 18:15:11 -07:00
10 changed files with 448 additions and 99 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+3 -1
View File
@@ -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
+10 -6
View File
@@ -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
+189 -1
View File
@@ -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
+15 -13
View File
@@ -8,23 +8,25 @@ from typing import List
from seedsigner.models.seed import Seed
def calculate_checksum(partial_mnemonic: list, wordlist_language_code: str) -> List[str]:
""" Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """
if len(partial_mnemonic) not in [11, 23]:
raise Exception("Pass in a 11- or 23-word mnemonic")
def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]:
"""
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
If 11- or 23-words are provided, append word `0000` to end of list as temp final
word.
"""
if len(mnemonic) in [11, 23]:
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0])
if len(mnemonic) not in [12, 24]:
raise Exception("Pass in a 12- or 24-word mnemonic")
# Work on a copy of the input list
mnemonic_copy = partial_mnemonic.copy()
# 12-word seeds contribute 7 bits of entropy to the final word; 24-word seeds
# contribute 3 bits. But we don't have any partial entropy bits to use to help us
# create the final word. So just default to filling those missing values with zeroes
# ("abandon" is word 0000, so effectively inserts zeroes).
mnemonic_copy.append("abandon")
mnemonic_copy = mnemonic.copy()
# Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation
# because we have to assume it's incorrect since we just hard-coded it above; we'll
# fix that next.
# because we assume it's incorrect since we either let the user select their own
# final word OR we injected the 0000 word from the wordlist.
mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code))
# This function will convert the bytes back into a mnemonic, but it will also
+9 -3
View File
@@ -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)
+63 -69
View File
@@ -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)
+137 -4
View File
@@ -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)),
+20
View File
@@ -43,6 +43,26 @@ def test_calculate_checksum():
def test_calculate_checksum_with_default_final_word():
""" 11-word and 23-word mnemonics use word `0000` as a temp final word to complete
the mnemonic.
"""
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert mnemonic1 == mnemonic2
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
partial_mnemonic += " abandon"
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert mnemonic1 == mnemonic2
def test_verify_against_coldcard_sample():
""" https://coldcard.com/docs/verifying-dice-roll-math """
dice_rolls = "123456"