from typing import TYPE_CHECKING from trezor import ui, utils, wire from trezor.enums import BackupType, ButtonRequestType from ...components.common.confirm import is_confirmed, raise_if_cancelled from ...components.tt.button import ButtonDefault from ...components.tt.checklist import Checklist from ...components.tt.confirm import Confirm, HoldToConfirm from ...components.tt.info import InfoConfirm from ...components.tt.reset import MnemonicWordSelect, Slip39NumInput from ...components.tt.scroll import Paginated from ...components.tt.text import Text from ..common import interact from . import confirm_action if TYPE_CHECKING: from typing import Sequence NumberedWords = Sequence[tuple[int, str]] if __debug__: from apps import debug async def show_share_words( ctx: wire.GenericContext, share_words: Sequence[str], share_index: int | None = None, group_index: int | None = None, ) -> None: first, chunks, last = _split_share_into_pages(share_words) if share_index is None: header_title = "Recovery seed" elif group_index is None: header_title = f"Recovery share #{share_index + 1}" else: header_title = f"Group {group_index + 1} - Share {share_index + 1}" header_icon = ui.ICON_RESET pages: list[ui.Component] = [] # ui page components shares_words_check = [] # check we display correct data # first page text = Text(header_title, header_icon) text.bold("Write down these") text.bold(f"{len(share_words)} words:") text.br_half() for index, word in first: text.mono(f"{index + 1}. {word}") shares_words_check.append(word) pages.append(text) # middle pages for chunk in chunks: text = Text(header_title, header_icon) for index, word in chunk: text.mono(f"{index + 1}. {word}") shares_words_check.append(word) pages.append(text) # last page text = Text(header_title, header_icon) for index, word in last: text.mono(f"{index + 1}. {word}") shares_words_check.append(word) text.br_half() text.bold(f"I wrote down all {len(share_words)}") text.bold("words in order.") pages.append(text) pages[-1] = HoldToConfirm(pages[-1], cancel=False) # pagination paginated = Paginated(pages) if __debug__: word_pages = [first] + chunks + [last] def export_displayed_words() -> None: # export currently displayed mnemonic words into debuglink words = [w for _, w in word_pages[paginated.page]] debug.reset_current_words.publish(words) paginated.on_change = export_displayed_words export_displayed_words() # make sure we display correct data utils.ensure(share_words == shares_words_check) # confirm the share await raise_if_cancelled( interact( ctx, paginated, "backup_words", ButtonRequestType.ResetDevice, ) ) async def select_word( ctx: wire.GenericContext, words: Sequence[str], share_index: int | None, checked_index: int, count: int, group_index: int | None = None, ) -> str: # let the user pick a word select = MnemonicWordSelect(words, share_index, checked_index, count, group_index) selected_word: str = await ctx.wait(select) return selected_word def _split_share_into_pages( share_words: Sequence[str], ) -> tuple[NumberedWords, list[NumberedWords], NumberedWords]: share = list(enumerate(share_words)) # we need to keep track of the word indices first = share[:2] # two words on the first page length = len(share_words) if length in (12, 20, 24): middle = share[2:-2] last = share[-2:] # two words on the last page elif length in (18, 33): middle = share[2:] last = [] # no words at the last page, because it does not add up else: # Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24 raise RuntimeError chunks = utils.chunks(middle, 4) # 4 words on the middle pages return first, list(chunks), last async def slip39_show_checklist( ctx: wire.GenericContext, step: int, backup_type: BackupType ) -> None: checklist = Checklist("Backup checklist", ui.ICON_RESET) if backup_type is BackupType.Slip39_Basic: checklist.add("Set number of shares") checklist.add("Set threshold") checklist.add(("Write down and check", "all recovery shares")) elif backup_type is BackupType.Slip39_Advanced: checklist.add("Set number of groups") checklist.add("Set group threshold") checklist.add(("Set size and threshold", "for each group")) checklist.select(step) await raise_if_cancelled( interact( ctx, Confirm(checklist, confirm="Continue", cancel=None), "slip39_checklist", ButtonRequestType.ResetDevice, ) ) async def slip39_prompt_threshold( ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None ) -> int: count = num_of_shares // 2 + 1 # min value of share threshold is 2 unless the number of shares is 1 # number of shares 1 is possible in advnaced slip39 min_count = min(2, num_of_shares) max_count = num_of_shares while True: shares = Slip39NumInput( Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id ) confirmed = is_confirmed( await interact( ctx, Confirm( shares, confirm="Continue", cancel="Info", major_confirm=True, cancel_style=ButtonDefault, ), "slip39_threshold", ButtonRequestType.ResetDevice, ) ) count = shares.input.count if confirmed: break text = "The threshold sets the number of shares " if group_id is None: text += "needed to recover your wallet. " text += f"Set it to {count} and you will need " if num_of_shares == 1: text += "1 share." elif num_of_shares == count: text += f"all {count} of your {num_of_shares} shares." else: text += f"any {count} of your {num_of_shares} shares." else: text += "needed to form a group. " text += f"Set it to {count} and you will " if num_of_shares == 1: text += "need 1 share " elif num_of_shares == count: text += f"need all {count} of {num_of_shares} shares " else: text += f"need any {count} of {num_of_shares} shares " text += f"to form Group {group_id + 1}." info = InfoConfirm(text) await info return count async def slip39_prompt_number_of_shares( ctx: wire.GenericContext, group_id: int | None = None ) -> int: count = 5 min_count = 1 max_count = 16 while True: shares = Slip39NumInput( Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id ) confirmed = is_confirmed( await interact( ctx, Confirm( shares, confirm="Continue", cancel="Info", major_confirm=True, cancel_style=ButtonDefault, ), "slip39_shares", ButtonRequestType.ResetDevice, ) ) count = shares.input.count if confirmed: break if group_id is None: info = InfoConfirm( "Each recovery share is a " "sequence of 20 words. " "Next you will choose " "how many shares you " "need to recover your " "wallet." ) else: info = InfoConfirm( "Each recovery share is a " "sequence of 20 words. " "Next you will choose " "the threshold number of " "shares needed to form " f"Group {group_id + 1}." ) await info return count async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int: count = 5 min_count = 2 max_count = 16 while True: shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count) confirmed = is_confirmed( await interact( ctx, Confirm( shares, confirm="Continue", cancel="Info", major_confirm=True, cancel_style=ButtonDefault, ), "slip39_groups", ButtonRequestType.ResetDevice, ) ) count = shares.input.count if confirmed: break info = InfoConfirm( "Each group has a set " "number of shares and " "its own threshold. In the " "next steps you will set " "the numbers of shares " "and the thresholds." ) await info return count async def slip39_advanced_prompt_group_threshold( ctx: wire.GenericContext, num_of_groups: int ) -> int: count = num_of_groups // 2 + 1 min_count = 1 max_count = num_of_groups while True: shares = Slip39NumInput( Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count ) confirmed = is_confirmed( await interact( ctx, Confirm( shares, confirm="Continue", cancel="Info", major_confirm=True, cancel_style=ButtonDefault, ), "slip39_group_threshold", ButtonRequestType.ResetDevice, ) ) count = shares.input.count if confirmed: break else: info = InfoConfirm( "The group threshold " "specifies the number of " "groups required to " "recover your wallet. " ) await info return count async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None: if slip39: description = "Never make a digital copy of your recovery shares and never upload them online!" else: description = "Never make a digital copy of your recovery seed and never upload\nit online!" await confirm_action( ctx, "backup_warning", "Caution", description=description, verb="I understand", verb_cancel=None, icon=ui.ICON_NOCOPY, br_code=ButtonRequestType.ResetDevice, )