1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-18 05:28:40 +00:00

refactor(core): convert apps.management.recovery_device to layouts

This commit is contained in:
Martin Milata 2021-03-24 18:03:24 +01:00
parent 312876ab67
commit 5a0ea3f146
9 changed files with 260 additions and 194 deletions

View File

@ -4,9 +4,8 @@ import storage.recovery
from trezor import config, ui, wire, workflow from trezor import config, ui, wire, workflow
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.messages import Success from trezor.messages import Success
from trezor.ui.components.tt.text import Text from trezor.ui.layouts import confirm_action, confirm_reset_device
from apps.common.confirm import require_confirm
from apps.common.request_pin import ( from apps.common.request_pin import (
error_pin_invalid, error_pin_invalid,
request_pin_and_sd_salt, request_pin_and_sd_salt,
@ -90,15 +89,15 @@ def _validate(msg: RecoveryDevice) -> None:
async def _continue_dialog(ctx: wire.Context, msg: RecoveryDevice) -> None: async def _continue_dialog(ctx: wire.Context, msg: RecoveryDevice) -> None:
if not msg.dry_run: if not msg.dry_run:
text = Text("Recovery mode", ui.ICON_RECOVERY, new_lines=False) await confirm_reset_device(
text.bold("Do you really want to recover a wallet?") ctx, "Do you really want to\nrecover a wallet?", recovery=True
text.br() )
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to ")
text.bold("https://trezor.io/tos")
else: else:
text = Text("Seed check", ui.ICON_RECOVERY, new_lines=False) await confirm_action(
text.normal("Do you really want to check the recovery seed?") ctx,
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) "confirm_seedcheck",
title="Seed check",
description="Do you really want to check the recovery seed?",
icon=ui.ICON_RECOVERY,
br_code=ButtonRequestType.ProtectCall,
)

View File

@ -150,8 +150,7 @@ async def _finish_recovery(
async def _request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: async def _request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
homepage = layout.RecoveryHomescreen("Select number of words") await layout.homescreen_dialog(ctx, "Select", "Select number of words")
await layout.homescreen_dialog(ctx, homepage, "Select")
# ask for the number of words # ask for the number of words
return await layout.request_word_count(ctx, dry_run) return await layout.request_word_count(ctx, dry_run)
@ -187,15 +186,13 @@ async def _request_share_first_screen(
if remaining: if remaining:
await _request_share_next_screen(ctx) await _request_share_next_screen(ctx)
else: else:
content = layout.RecoveryHomescreen( await layout.homescreen_dialog(
"Enter any share", "(%d words)" % word_count ctx, "Enter share", "Enter any share", "(%d words)" % word_count
) )
await layout.homescreen_dialog(ctx, content, "Enter share")
else: # BIP-39 else: # BIP-39
content = layout.RecoveryHomescreen( await layout.homescreen_dialog(
"Enter recovery seed", "(%d words)" % word_count ctx, "Enter seed", "Enter recovery seed", "(%d words)" % word_count
) )
await layout.homescreen_dialog(ctx, content, "Enter seed")
async def _request_share_next_screen(ctx: wire.GenericContext) -> None: async def _request_share_next_screen(ctx: wire.GenericContext) -> None:
@ -206,14 +203,15 @@ async def _request_share_next_screen(ctx: wire.GenericContext) -> None:
raise RuntimeError raise RuntimeError
if group_count > 1: if group_count > 1:
content = layout.RecoveryHomescreen("More shares needed")
await layout.homescreen_dialog( await layout.homescreen_dialog(
ctx, content, "Enter", _show_remaining_groups_and_shares ctx,
"Enter",
"More shares needed",
info_func=_show_remaining_groups_and_shares,
) )
else: else:
text = strings.format_plural("{count} more {plural}", remaining[0], "share") text = strings.format_plural("{count} more {plural}", remaining[0], "share")
content = layout.RecoveryHomescreen(text, "needed to enter") await layout.homescreen_dialog(ctx, "Enter share", text, "needed to enter")
await layout.homescreen_dialog(ctx, content, "Enter share")
async def _show_remaining_groups_and_shares(ctx: wire.GenericContext) -> None: async def _show_remaining_groups_and_shares(ctx: wire.GenericContext) -> None:

View File

@ -1,23 +1,22 @@
import storage.recovery import storage.recovery
from trezor import strings, ui, wire from trezor import ui, wire
from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.ui.components.tt.scroll import Paginated
from trezor.ui.components.tt.text import Text
from trezor.ui.components.tt.word_select import WordSelector
from trezor.ui.layouts import confirm_action, show_success, show_warning from trezor.ui.layouts import confirm_action, show_success, show_warning
from trezor.ui.layouts.common import button_request from trezor.ui.layouts.common import button_request
from trezor.ui.layouts.tt.recovery import ( # noqa: F401
from apps.common.confirm import confirm, info_confirm, require_confirm continue_recovery,
request_word,
request_word_count,
show_group_share_success,
show_remaining_shares,
)
from .. import backup_types from .. import backup_types
from . import word_validity from . import word_validity
from .keyboard_bip39 import Bip39Keyboard
from .keyboard_slip39 import Slip39Keyboard
from .recover import RecoveryAborted from .recover import RecoveryAborted
if False: if False:
from typing import Callable, Iterable from typing import Callable
from trezor.enums import BackupType from trezor.enums import BackupType
@ -44,37 +43,16 @@ async def confirm_abort(ctx: wire.GenericContext, dry_run: bool = False) -> None
) )
async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
await button_request(ctx, code=ButtonRequestType.MnemonicWordCount)
if dry_run:
text = Text("Seed check", ui.ICON_RECOVERY)
else:
text = Text("Recovery mode", ui.ICON_RECOVERY)
text.normal("Number of words?")
count = await ctx.wait(WordSelector(text))
# WordSelector can return int, or string if the value came from debuglink
# ctx.wait has a return type Any
# Hence, it is easier to convert the returned value to int explicitly
return int(count)
async def request_mnemonic( async def request_mnemonic(
ctx: wire.GenericContext, word_count: int, backup_type: BackupType | None ctx: wire.GenericContext, word_count: int, backup_type: BackupType | None
) -> str | None: ) -> str | None:
await button_request(ctx, code=ButtonRequestType.MnemonicInput) await button_request(ctx, "mnemonic", code=ButtonRequestType.MnemonicInput)
words: list[str] = [] words: list[str] = []
for i in range(word_count): for i in range(word_count):
if backup_types.is_slip39_word_count(word_count): word = await request_word(
keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard( ctx, i, word_count, is_slip39=backup_types.is_slip39_word_count(word_count)
"Type word %s of %s:" % (i + 1, word_count)
) )
else:
keyboard = Bip39Keyboard("Type word %s of %s:" % (i + 1, word_count))
word = await ctx.wait(keyboard)
words.append(word) words.append(word)
try: try:
@ -92,52 +70,6 @@ async def request_mnemonic(
return " ".join(words) return " ".join(words)
async def show_remaining_shares(
ctx: wire.GenericContext,
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
shares_remaining: list[int],
group_threshold: int,
) -> None:
pages: list[ui.Component] = []
for remaining, group in groups:
if 0 < remaining < MAX_SHARE_COUNT:
text = Text("Remaining Shares")
text.bold(
strings.format_plural(
"{count} more {plural} starting", remaining, "share"
)
)
for word in group:
text.normal(word)
pages.append(text)
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
text = Text("Remaining Shares")
groups_remaining = group_threshold - shares_remaining.count(0)
text.bold(
strings.format_plural(
"{count} more {plural} starting", groups_remaining, "group"
)
)
for word in group:
text.normal(word)
pages.append(text)
await confirm(ctx, Paginated(pages), cancel=None)
async def show_group_share_success(
ctx: wire.GenericContext, share_index: int, group_index: int
) -> None:
text = Text("Success", ui.ICON_CONFIRM)
text.bold("You have entered")
text.bold("Share %s" % (share_index + 1))
text.normal("from")
text.bold("Group %s" % (group_index + 1))
await confirm(ctx, text, confirm="Continue", cancel=None)
async def show_dry_run_result( async def show_dry_run_result(
ctx: wire.GenericContext, result: bool, is_slip39: bool ctx: wire.GenericContext, result: bool, is_slip39: bool
) -> None: ) -> None:
@ -156,12 +88,14 @@ async def show_dry_run_result(
async def show_dry_run_different_type(ctx: wire.GenericContext) -> None: async def show_dry_run_different_type(ctx: wire.GenericContext) -> None:
text = Text("Dry run failure", ui.ICON_CANCEL) await show_warning(
text.normal("Seed in the device was") ctx,
text.normal("created using another") "warning_dry_recovery",
text.normal("backup mechanism.") header="Dry run failure",
await require_confirm( content="Seed in the device was\ncreated using another\nbackup mechanism.",
ctx, text, ButtonRequestType.ProtectCall, cancel=None, confirm="Continue" icon=ui.ICON_CANCEL,
icon_color=ui.ORANGE_ICON,
br_code=ButtonRequestType.ProtectCall,
) )
@ -204,72 +138,15 @@ async def show_group_threshold_reached(ctx: wire.GenericContext) -> None:
) )
class RecoveryHomescreen(ui.Component):
def __init__(self, text: str, subtext: str | None = None):
super().__init__()
self.text = text
self.subtext = subtext
self.dry_run = storage.recovery.is_dry_run()
def on_render(self) -> None:
if not self.repaint:
return
if self.dry_run:
heading = "SEED CHECK"
else:
heading = "RECOVERY MODE"
ui.header_warning(heading, clear=False)
if not self.subtext:
ui.display.text_center(ui.WIDTH // 2, 80, self.text, ui.BOLD, ui.FG, ui.BG)
else:
ui.display.text_center(ui.WIDTH // 2, 65, self.text, ui.BOLD, ui.FG, ui.BG)
ui.display.text_center(
ui.WIDTH // 2, 92, self.subtext, ui.NORMAL, ui.FG, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 130, "It is safe to eject Trezor", ui.NORMAL, ui.GREY, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 155, "and continue later", ui.NORMAL, ui.GREY, ui.BG
)
self.repaint = False
if __debug__:
def read_content(self) -> list[str]:
return [self.__class__.__name__, self.text, self.subtext or ""]
async def homescreen_dialog( async def homescreen_dialog(
ctx: wire.GenericContext, ctx: wire.GenericContext,
homepage: RecoveryHomescreen,
button_label: str, button_label: str,
text: str,
subtext: str | None = None,
info_func: Callable | None = None, info_func: Callable | None = None,
) -> None: ) -> None:
while True: while True:
if info_func: if await continue_recovery(ctx, button_label, text, subtext, info_func):
continue_recovery = await info_confirm(
ctx,
homepage,
code=ButtonRequestType.RecoveryHomepage,
confirm=button_label,
info_func=info_func,
info="Info",
cancel="Abort",
)
else:
continue_recovery = await confirm(
ctx,
homepage,
code=ButtonRequestType.RecoveryHomepage,
confirm=button_label,
major_confirm=True,
)
if continue_recovery:
# go forward in the recovery process # go forward in the recovery process
break break
# user has chosen to abort, confirm the choice # user has chosen to abort, confirm the choice

View File

@ -1,7 +1,7 @@
from trezor import loop, ui, wire from trezor import loop, ui, wire
if False: if False:
from typing import Any, Awaitable from typing import Callable, Any, Awaitable
CONFIRMED = object() CONFIRMED = object()
CANCELLED = object() CANCELLED = object()
@ -18,6 +18,20 @@ async def raise_if_cancelled(a: Awaitable, exc: Any = wire.ActionCancelled) -> N
raise exc raise exc
async def is_confirmed_info(
ctx: wire.GenericContext,
dialog: ui.Layout,
info_func: Callable,
) -> bool:
while True:
result = await ctx.wait(dialog)
if result is INFO:
await info_func(ctx)
else:
return is_confirmed(result)
class ConfirmBase(ui.Layout): class ConfirmBase(ui.Layout):
def __init__( def __init__(
self, self,

View File

@ -1,15 +1,11 @@
from trezor import io, loop, res, ui, workflow from trezor import io, loop, res, ui, workflow
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.ui import display from trezor.ui import display
from trezor.ui.components.tt.button import (
Button, from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
ButtonClear,
ButtonMono,
ButtonMonoConfirm,
)
if False: if False:
from trezor.ui.components.tt.button import ButtonContent, ButtonStyleStateType from .button import ButtonContent, ButtonStyleStateType
def compute_mask(text: str) -> int: def compute_mask(text: str) -> int:

View File

@ -1,15 +1,11 @@
from trezor import io, loop, res, ui, workflow from trezor import io, loop, res, ui, workflow
from trezor.crypto import slip39 from trezor.crypto import slip39
from trezor.ui import display from trezor.ui import display
from trezor.ui.components.tt.button import (
Button, from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
ButtonClear,
ButtonMono,
ButtonMonoConfirm,
)
if False: if False:
from trezor.ui.components.tt.button import ButtonContent, ButtonStyleStateType from .button import ButtonContent, ButtonStyleStateType
class KeyButton(Button): class KeyButton(Button):

View File

@ -0,0 +1,42 @@
import storage.recovery
from trezor import ui
class RecoveryHomescreen(ui.Component):
def __init__(self, text: str, subtext: str | None = None):
super().__init__()
self.text = text
self.subtext = subtext
self.dry_run = storage.recovery.is_dry_run()
def on_render(self) -> None:
if not self.repaint:
return
if self.dry_run:
heading = "SEED CHECK"
else:
heading = "RECOVERY MODE"
ui.header_warning(heading, clear=False)
if not self.subtext:
ui.display.text_center(ui.WIDTH // 2, 80, self.text, ui.BOLD, ui.FG, ui.BG)
else:
ui.display.text_center(ui.WIDTH // 2, 65, self.text, ui.BOLD, ui.FG, ui.BG)
ui.display.text_center(
ui.WIDTH // 2, 92, self.subtext, ui.NORMAL, ui.FG, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 130, "It is safe to eject Trezor", ui.NORMAL, ui.GREY, ui.BG
)
ui.display.text_center(
ui.WIDTH // 2, 155, "and continue later", ui.NORMAL, ui.GREY, ui.BG
)
self.repaint = False
if __debug__:
def read_content(self) -> list[str]:
return [self.__class__.__name__, self.text, self.subtext or ""]

View File

@ -149,7 +149,12 @@ async def confirm_action(
) )
async def confirm_reset_device(ctx: wire.GenericContext, prompt: str) -> None: async def confirm_reset_device(
ctx: wire.GenericContext, prompt: str, recovery: bool = False
) -> None:
if recovery:
text = Text("Recovery mode", ui.ICON_RECOVERY, new_lines=False)
else:
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False) text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
text.bold(prompt) text.bold(prompt)
text.br() text.br()
@ -161,9 +166,11 @@ async def confirm_reset_device(ctx: wire.GenericContext, prompt: str) -> None:
await raise_if_cancelled( await raise_if_cancelled(
interact( interact(
ctx, ctx,
Confirm(text, major_confirm=True), Confirm(text, major_confirm=not recovery),
"setup_device", "recover_device" if recovery else "setup_device",
ButtonRequestType.ResetDevice, ButtonRequestType.ProtectCall
if recovery
else ButtonRequestType.ResetDevice,
) )
) )
@ -442,6 +449,8 @@ def show_warning(
subheader: str | None = None, subheader: str | None = None,
button: str = "Try again", button: str = "Try again",
br_code: ButtonRequestType = ButtonRequestType.Warning, br_code: ButtonRequestType = ButtonRequestType.Warning,
icon: str = ui.ICON_WRONG,
icon_color: int = ui.RED,
) -> Awaitable[None]: ) -> Awaitable[None]:
return _show_modal( return _show_modal(
ctx, ctx,
@ -452,8 +461,8 @@ def show_warning(
content=content, content=content,
button_confirm=button, button_confirm=button,
button_cancel=None, button_cancel=None,
icon=ui.ICON_WRONG, icon=icon,
icon_color=ui.RED, icon_color=icon_color,
) )

View File

@ -0,0 +1,135 @@
from trezor import strings, ui, wire
from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor.enums import ButtonRequestType
from ...components.common.confirm import (
is_confirmed,
is_confirmed_info,
raise_if_cancelled,
)
from ...components.tt.confirm import Confirm, InfoConfirm
from ...components.tt.keyboard_bip39 import Bip39Keyboard
from ...components.tt.keyboard_slip39 import Slip39Keyboard
from ...components.tt.recovery import RecoveryHomescreen
from ...components.tt.scroll import Paginated
from ...components.tt.text import Text
from ...components.tt.word_select import WordSelector
from ..common import button_request, interact
if False:
from typing import Callable, Iterable
async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount)
if dry_run:
text = Text("Seed check", ui.ICON_RECOVERY)
else:
text = Text("Recovery mode", ui.ICON_RECOVERY)
text.normal("Number of words?")
count = await ctx.wait(WordSelector(text))
# WordSelector can return int, or string if the value came from debuglink
# ctx.wait has a return type Any
# Hence, it is easier to convert the returned value to int explicitly
return int(count)
async def request_word(
ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool
) -> str:
if is_slip39:
keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard(
"Type word %s of %s:" % (word_index + 1, word_count)
)
else:
keyboard = Bip39Keyboard("Type word %s of %s:" % (word_index + 1, word_count))
word: str = await ctx.wait(keyboard)
return word
async def show_remaining_shares(
ctx: wire.GenericContext,
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
shares_remaining: list[int],
group_threshold: int,
) -> None:
pages: list[ui.Component] = []
for remaining, group in groups:
if 0 < remaining < MAX_SHARE_COUNT:
text = Text("Remaining Shares")
text.bold(
strings.format_plural(
"{count} more {plural} starting", remaining, "share"
)
)
for word in group:
text.normal(word)
pages.append(text)
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
text = Text("Remaining Shares")
groups_remaining = group_threshold - shares_remaining.count(0)
text.bold(
strings.format_plural(
"{count} more {plural} starting", groups_remaining, "group"
)
)
for word in group:
text.normal(word)
pages.append(text)
pages[-1] = Confirm(pages[-1], cancel=None)
await raise_if_cancelled(
interact(ctx, Paginated(pages), "show_shares", ButtonRequestType.Other)
)
async def show_group_share_success(
ctx: wire.GenericContext, share_index: int, group_index: int
) -> None:
text = Text("Success", ui.ICON_CONFIRM)
text.bold("You have entered")
text.bold("Share %s" % (share_index + 1))
text.normal("from")
text.bold("Group %s" % (group_index + 1))
await raise_if_cancelled(
interact(
ctx,
Confirm(text, confirm="Continue", cancel=None),
"share_success",
ButtonRequestType.Other,
)
)
async def continue_recovery(
ctx: wire.GenericContext,
button_label: str,
text: str,
subtext: str | None,
info_func: Callable | None,
) -> bool:
homepage = RecoveryHomescreen(text, subtext)
if info_func is not None:
content = InfoConfirm(
homepage,
confirm=button_label,
info="Info",
cancel="Abort",
)
await button_request(ctx, "recovery", ButtonRequestType.RecoveryHomepage)
return await is_confirmed_info(ctx, content, info_func)
else:
return is_confirmed(
await interact(
ctx,
Confirm(homepage, confirm=button_label, major_confirm=True),
"recovery",
ButtonRequestType.RecoveryHomepage,
)
)