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.enums import ButtonRequestType
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 (
error_pin_invalid,
request_pin_and_sd_salt,
@ -90,15 +89,15 @@ def _validate(msg: RecoveryDevice) -> None:
async def _continue_dialog(ctx: wire.Context, msg: RecoveryDevice) -> None:
if not msg.dry_run:
text = Text("Recovery mode", ui.ICON_RECOVERY, new_lines=False)
text.bold("Do you really want to recover a wallet?")
text.br()
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to ")
text.bold("https://trezor.io/tos")
await confirm_reset_device(
ctx, "Do you really want to\nrecover a wallet?", recovery=True
)
else:
text = Text("Seed check", ui.ICON_RECOVERY, new_lines=False)
text.normal("Do you really want to check the recovery seed?")
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await confirm_action(
ctx,
"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:
homepage = layout.RecoveryHomescreen("Select number of words")
await layout.homescreen_dialog(ctx, homepage, "Select")
await layout.homescreen_dialog(ctx, "Select", "Select number of words")
# ask for the number of words
return await layout.request_word_count(ctx, dry_run)
@ -187,15 +186,13 @@ async def _request_share_first_screen(
if remaining:
await _request_share_next_screen(ctx)
else:
content = layout.RecoveryHomescreen(
"Enter any share", "(%d words)" % word_count
await layout.homescreen_dialog(
ctx, "Enter share", "Enter any share", "(%d words)" % word_count
)
await layout.homescreen_dialog(ctx, content, "Enter share")
else: # BIP-39
content = layout.RecoveryHomescreen(
"Enter recovery seed", "(%d words)" % word_count
await layout.homescreen_dialog(
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:
@ -206,14 +203,15 @@ async def _request_share_next_screen(ctx: wire.GenericContext) -> None:
raise RuntimeError
if group_count > 1:
content = layout.RecoveryHomescreen("More shares needed")
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:
text = strings.format_plural("{count} more {plural}", remaining[0], "share")
content = layout.RecoveryHomescreen(text, "needed to enter")
await layout.homescreen_dialog(ctx, content, "Enter share")
await layout.homescreen_dialog(ctx, "Enter share", text, "needed to enter")
async def _show_remaining_groups_and_shares(ctx: wire.GenericContext) -> None:

View File

@ -1,23 +1,22 @@
import storage.recovery
from trezor import strings, ui, wire
from trezor.crypto.slip39 import MAX_SHARE_COUNT
from trezor import ui, wire
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.common import button_request
from apps.common.confirm import confirm, info_confirm, require_confirm
from trezor.ui.layouts.tt.recovery import ( # noqa: F401
continue_recovery,
request_word,
request_word_count,
show_group_share_success,
show_remaining_shares,
)
from .. import backup_types
from . import word_validity
from .keyboard_bip39 import Bip39Keyboard
from .keyboard_slip39 import Slip39Keyboard
from .recover import RecoveryAborted
if False:
from typing import Callable, Iterable
from typing import Callable
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(
ctx: wire.GenericContext, word_count: int, backup_type: BackupType | None
) -> str | None:
await button_request(ctx, code=ButtonRequestType.MnemonicInput)
await button_request(ctx, "mnemonic", code=ButtonRequestType.MnemonicInput)
words: list[str] = []
for i in range(word_count):
if backup_types.is_slip39_word_count(word_count):
keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard(
"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)
word = await request_word(
ctx, i, word_count, is_slip39=backup_types.is_slip39_word_count(word_count)
)
words.append(word)
try:
@ -92,52 +70,6 @@ async def request_mnemonic(
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(
ctx: wire.GenericContext, result: bool, is_slip39: bool
) -> None:
@ -156,12 +88,14 @@ async def show_dry_run_result(
async def show_dry_run_different_type(ctx: wire.GenericContext) -> None:
text = Text("Dry run failure", ui.ICON_CANCEL)
text.normal("Seed in the device was")
text.normal("created using another")
text.normal("backup mechanism.")
await require_confirm(
ctx, text, ButtonRequestType.ProtectCall, cancel=None, confirm="Continue"
await show_warning(
ctx,
"warning_dry_recovery",
header="Dry run failure",
content="Seed in the device was\ncreated using another\nbackup mechanism.",
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(
ctx: wire.GenericContext,
homepage: RecoveryHomescreen,
button_label: str,
text: str,
subtext: str | None = None,
info_func: Callable | None = None,
) -> None:
while True:
if 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:
if await continue_recovery(ctx, button_label, text, subtext, info_func):
# go forward in the recovery process
break
# user has chosen to abort, confirm the choice

View File

@ -1,7 +1,7 @@
from trezor import loop, ui, wire
if False:
from typing import Any, Awaitable
from typing import Callable, Any, Awaitable
CONFIRMED = object()
CANCELLED = object()
@ -18,6 +18,20 @@ async def raise_if_cancelled(a: Awaitable, exc: Any = wire.ActionCancelled) -> N
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):
def __init__(
self,

View File

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

View File

@ -1,15 +1,11 @@
from trezor import io, loop, res, ui, workflow
from trezor.crypto import slip39
from trezor.ui import display
from trezor.ui.components.tt.button import (
Button,
ButtonClear,
ButtonMono,
ButtonMonoConfirm,
)
from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
if False:
from trezor.ui.components.tt.button import ButtonContent, ButtonStyleStateType
from .button import ButtonContent, ButtonStyleStateType
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,8 +149,13 @@ async def confirm_action(
)
async def confirm_reset_device(ctx: wire.GenericContext, prompt: str) -> None:
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
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.bold(prompt)
text.br()
text.br_half()
@ -161,9 +166,11 @@ async def confirm_reset_device(ctx: wire.GenericContext, prompt: str) -> None:
await raise_if_cancelled(
interact(
ctx,
Confirm(text, major_confirm=True),
"setup_device",
ButtonRequestType.ResetDevice,
Confirm(text, major_confirm=not recovery),
"recover_device" if recovery else "setup_device",
ButtonRequestType.ProtectCall
if recovery
else ButtonRequestType.ResetDevice,
)
)
@ -442,6 +449,8 @@ def show_warning(
subheader: str | None = None,
button: str = "Try again",
br_code: ButtonRequestType = ButtonRequestType.Warning,
icon: str = ui.ICON_WRONG,
icon_color: int = ui.RED,
) -> Awaitable[None]:
return _show_modal(
ctx,
@ -452,8 +461,8 @@ def show_warning(
content=content,
button_confirm=button,
button_cancel=None,
icon=ui.ICON_WRONG,
icon_color=ui.RED,
icon=icon,
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,
)
)