TR-core/UI: add TR files into trezor/ui/layouts/tr + helpers changes

grdddj/debuglink_improvements
grdddj 1 year ago
parent 0641170751
commit 7a59e716bb

@ -163,6 +163,16 @@ trezor.ui.layouts.reset
import trezor.ui.layouts.reset
trezor.ui.layouts.tr
import trezor.ui.layouts.tr
trezor.ui.layouts.tr.fido
import trezor.ui.layouts.tr.fido
trezor.ui.layouts.tr.homescreen
import trezor.ui.layouts.tr.homescreen
trezor.ui.layouts.tr.progress
import trezor.ui.layouts.tr.progress
trezor.ui.layouts.tr.recovery
import trezor.ui.layouts.tr.recovery
trezor.ui.layouts.tr.reset
import trezor.ui.layouts.tr.reset
trezor.ui.layouts.tt_v2
import trezor.ui.layouts.tt_v2
trezor.ui.layouts.tt_v2.fido

@ -292,6 +292,7 @@ async def confirm_nondefault_locktime(
ctx: Context, lock_time: int, lock_time_disabled: bool
) -> None:
from trezor.strings import format_timestamp
from trezor import utils
if lock_time_disabled:
title = "Warning"
@ -306,6 +307,10 @@ async def confirm_nondefault_locktime(
text = "Locktime for this transaction is set to:\n{}"
param = format_timestamp(lock_time)
# Getting rid of newlines for TR, to fit one smaller screen:
if utils.MODEL in ("R",):
text = text.replace("\n", " ")
await confirm_metadata(
ctx,
"nondefault_locktime",

@ -8,9 +8,9 @@ if __debug__:
import trezorui2
from trezor import log, loop, wire
from trezor import log, loop, utils, wire
from trezor.ui import display
from trezor.enums import MessageType
from trezor.enums import MessageType, DebugPhysicalButton
from trezor.messages import (
DebugLinkLayout,
Success,
@ -38,9 +38,11 @@ if __debug__:
confirm_chan = loop.chan()
swipe_chan = loop.chan()
input_chan = loop.chan()
model_r_btn_chan = loop.chan()
confirm_signal = confirm_chan.take
swipe_signal = swipe_chan.take
input_signal = input_chan.take
model_r_btn_signal = model_r_btn_chan.take
debuglink_decision_chan = loop.chan()
@ -74,6 +76,8 @@ if __debug__:
await confirm_chan.put(Result(trezorui2.CONFIRMED))
elif msg.button == DebugButton.INFO:
await confirm_chan.put(Result(trezorui2.INFO))
if msg.physical_button is not None:
await model_r_btn_chan.put(msg.physical_button)
if msg.swipe is not None:
await swipe_chan.put(msg.swipe)
if msg.input is not None:
@ -101,6 +105,12 @@ if __debug__:
await loop.sleep(duration_ms)
loop.synthetic_events.append((io.TOUCH, (io.TOUCH_END, x, y)))
async def button_hold(btn: int, duration_ms: int) -> None:
from trezor import io
await loop.sleep(duration_ms)
loop.synthetic_events.append((io.BUTTON, (io.BUTTON_RELEASED, btn)))
async def dispatch_DebugLinkWatchLayout(
ctx: wire.Context, msg: DebugLinkWatchLayout
) -> Success:
@ -127,7 +137,7 @@ if __debug__:
y = msg.y # local_cache_attribute
# TT click on specific coordinates, with possible hold
if x is not None and y is not None:
if x is not None and y is not None and utils.MODEL in ("T",):
evt_down = io.TOUCH_START, x, y
evt_up = io.TOUCH_END, x, y
loop.synthetic_events.append((io.TOUCH, evt_down))
@ -135,6 +145,20 @@ if __debug__:
loop.schedule(touch_hold(x, y, msg.hold_ms))
else:
loop.synthetic_events.append((io.TOUCH, evt_up))
# TR hold of a specific button
elif (
msg.physical_button is not None
and msg.hold_ms is not None
and utils.MODEL in ("R",)
):
if msg.physical_button == DebugPhysicalButton.LEFT_BTN:
btn = io.BUTTON_LEFT
elif msg.physical_button == DebugPhysicalButton.RIGHT_BTN:
btn = io.BUTTON_RIGHT
else:
raise wire.ProcessError("Unknown physical button")
loop.synthetic_events.append((io.BUTTON, (io.BUTTON_PRESSED, btn)))
loop.schedule(button_hold(btn, msg.hold_ms))
# Something more general
else:
debuglink_decision_chan.publish(msg)

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import show_warning
from trezor.ui.layouts import confirm_action, show_warning
from trezor.ui.layouts.recovery import ( # noqa: F401
request_word_count,
show_group_share_success,
@ -45,8 +45,20 @@ async def request_mnemonic(
from . import word_validity
from trezor.ui.layouts.common import button_request
from trezor.ui.layouts.recovery import request_word
from trezor import utils
await button_request(ctx, "mnemonic", ButtonRequestType.MnemonicInput)
if utils.MODEL in ("R",):
await confirm_action(
ctx,
"request_word",
"WORD ENTERING",
description="You'll only have to select the first 2-3 letters.",
verb="CONTINUE",
verb_cancel=None,
br_code=ButtonRequestType.MnemonicInput,
)
await button_request(ctx, "mnemonic", code=ButtonRequestType.MnemonicInput)
words: list[str] = []
for i in range(word_count):

@ -196,7 +196,7 @@ def get_passphrase_always_on_device() -> bool:
from trezor import utils
# Some models do not support passphrase input on device
if utils.MODEL in ("1", "R"):
if utils.MODEL in ("1",):
return False
return common.get_bool(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE)

@ -21,12 +21,10 @@ def format_amount(amount: int, decimals: int) -> str:
return s
if False: # noqa
def format_ordinal(number: int) -> str:
return str(number) + {1: "st", 2: "nd", 3: "rd"}.get(
4 if 10 <= number % 100 < 20 else number % 10, "th"
)
def format_ordinal(number: int) -> str:
return str(number) + {1: "st", 2: "nd", 3: "rd"}.get(
4 if 10 <= number % 100 < 20 else number % 10, "th"
)
def format_plural(string: str, count: int, plural: str) -> str:

File diff suppressed because it is too large Load Diff

@ -0,0 +1,55 @@
from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType
import trezorui2
from ..common import interact
from . import RustLayout
if TYPE_CHECKING:
from trezor.wire import GenericContext
async def confirm_fido(
ctx: GenericContext | None,
header: str,
app_name: str,
icon_name: str | None,
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = RustLayout(
trezorui2.confirm_fido( # type: ignore [Arguments missing]
app_name=app_name,
accounts=accounts,
)
)
if ctx is None:
result = await confirm
else:
result = await interact(ctx, confirm, "confirm_fido", ButtonRequestType.Other)
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
return result
# Late import won't get executed on the happy path.
from trezor.wire import ActionCancelled
raise ActionCancelled
async def confirm_fido_reset() -> bool:
confirm = RustLayout(
trezorui2.confirm_action(
title="FIDO2 RESET",
description="Do you really want to erase all credentials?",
action=None,
verb_cancel="",
verb="CONFIRM",
)
)
return (await confirm) is trezorui2.CONFIRMED

@ -0,0 +1,134 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
from trezor import ui
import trezorui2
from . import RustLayout
if TYPE_CHECKING:
from trezor import loop
from typing import Any, Tuple
class HomescreenBase(RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _paint(self) -> None:
self.layout.paint()
ui.refresh()
def _first_paint(self) -> None:
if storage_cache.homescreen_shown is not self.RENDER_INDICATOR:
super()._first_paint()
storage_cache.homescreen_shown = self.RENDER_INDICATOR
else:
self._paint()
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return self.handle_timers(), self.handle_input_and_rendering()
class Homescreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.HOMESCREEN_ON
def __init__(
self,
label: str | None,
notification: str | None,
notification_is_error: bool,
hold_to_lock: bool,
) -> None:
level = 1
if notification is not None:
notification = notification.rstrip("!")
if "EXPERIMENTAL" in notification:
level = 2
elif notification_is_error:
level = 0
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_homescreen(
label=label or "My Trezor",
notification=notification,
notification_level=level,
hold=hold_to_lock,
skip_first_paint=skip,
),
)
async def usb_checker_task(self) -> None:
from trezor import io, loop
usbcheck = loop.wait(io.USB_CHECK)
while True:
is_connected = await usbcheck
self.layout.usb_event(is_connected)
self.layout.paint()
ui.refresh()
def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]:
return super().create_tasks() + (self.usb_checker_task(),)
class Lockscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON
BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW
def __init__(
self,
label: str | None,
bootscreen: bool = False,
) -> None:
self.bootscreen = bootscreen
if bootscreen:
self.BACKLIGHT_LEVEL = ui.BACKLIGHT_NORMAL
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label or "My Trezor",
bootscreen=bootscreen,
skip_first_paint=skip,
),
)
async def __iter__(self) -> Any:
result = await super().__iter__()
if self.bootscreen:
self.request_complete_repaint()
return result
class Busyscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON
def __init__(self, delay_ms: int) -> None:
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
# TODO: remove show_busyscreen in favor of show_progress_coinjoin
layout=trezorui2.show_busyscreen(
title="PLEASE WAIT",
description="Coinjoin in progress.\n\nDo not disconnect your Trezor.",
time_ms=delay_ms,
skip_first_paint=skip,
)
)
async def __iter__(self) -> Any:
from apps.base import set_homescreen
# Handle timeout.
result = await super().__iter__()
assert result == trezorui2.CANCELLED
storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
return result

@ -0,0 +1,74 @@
from typing import TYPE_CHECKING
from trezor import ui
import trezorui2
if TYPE_CHECKING:
from typing import Any
from ..common import ProgressLayout
class RustProgress:
def __init__(
self,
layout: Any,
):
self.layout = layout
ui.backlight_fade(ui.style.BACKLIGHT_DIM)
ui.display.clear()
self.layout.attach_timer_fn(self.set_timer)
self.layout.paint()
ui.backlight_fade(ui.style.BACKLIGHT_NORMAL)
def set_timer(self, token: int, deadline: int) -> None:
raise RuntimeError # progress layouts should not set timers
def report(self, value: int, description: str | None = None):
msg = self.layout.progress_event(value, description or "")
assert msg is None
self.layout.paint()
ui.refresh()
def progress(
message: str = "PLEASE WAIT",
description: str | None = None,
indeterminate: bool = False,
) -> ProgressLayout:
return RustProgress(
layout=trezorui2.show_progress(
title=message.upper(),
indeterminate=indeterminate,
description=description or "",
)
)
def bitcoin_progress(message: str) -> ProgressLayout:
return progress(message)
def coinjoin_progress(message: str) -> ProgressLayout:
# TODO: create show_progress_coinjoin for TR
return progress(message, description="Coinjoin")
# return RustProgress(
# layout=trezorui2.show_progress_coinjoin(title=message, indeterminate=False)
# )
def pin_progress(message: str, description: str) -> ProgressLayout:
return progress(message, description=description)
def monero_keyimage_sync_progress() -> ProgressLayout:
return progress("SYNCING")
def monero_live_refresh_progress() -> ProgressLayout:
return progress("REFRESHING", indeterminate=True)
def monero_transaction_progress_inner() -> ProgressLayout:
return progress("SIGNING TRANSACTION")

@ -0,0 +1,103 @@
from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType
import trezorui2
from ..common import button_request, interact
from . import RustLayout, raise_if_not_confirmed
if TYPE_CHECKING:
from trezor import wire
from typing import Iterable, Callable
async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount)
count = await interact(
ctx,
RustLayout(trezorui2.select_word_count(dry_run=dry_run)),
"word_count",
ButtonRequestType.MnemonicWordCount,
)
# It can be returning a string (for example for __debug__ in tests)
return int(count)
async def request_word(
ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool
) -> str:
prompt = f"WORD {word_index + 1} OF {word_count}"
if is_slip39:
word_choice = RustLayout(trezorui2.request_slip39(prompt=prompt))
else:
word_choice = RustLayout(trezorui2.request_bip39(prompt=prompt))
word: str = await ctx.wait(word_choice)
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:
raise NotImplementedError
async def show_group_share_success(
ctx: wire.GenericContext, share_index: int, group_index: int
) -> None:
await raise_if_not_confirmed(
interact(
ctx,
RustLayout(
trezorui2.show_group_share_success(
lines=[
"You have entered",
f"Share {share_index + 1}",
"from",
f"Group {group_index + 1}",
],
)
),
"share_success",
ButtonRequestType.Other,
)
)
async def continue_recovery(
ctx: wire.GenericContext,
button_label: str,
text: str,
subtext: str | None,
info_func: Callable | None,
dry_run: bool,
) -> bool:
# TODO: implement info_func?
# There is very limited space on the screen
# (and having middle button would mean shortening the right button text)
description = text
if subtext:
description += f"\n\n{subtext}"
homepage = RustLayout(
trezorui2.confirm_recovery(
title="",
description=description,
button=button_label.upper(),
info_button=False,
dry_run=dry_run,
)
)
result = await interact(
ctx,
homepage,
"recovery",
ButtonRequestType.RecoveryHomepage,
)
return result is trezorui2.CONFIRMED

@ -0,0 +1,286 @@
from typing import TYPE_CHECKING
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
import trezorui2
from ..common import interact
from . import RustLayout, confirm_action
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
if TYPE_CHECKING:
from trezor.wire import GenericContext
from trezor.enums import BackupType
from typing import Sequence
async def show_share_words(
ctx: GenericContext,
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
from . import get_bool
if share_index is None:
title = "RECOVERY SEED"
elif group_index is None:
title = f"SHARE #{share_index + 1}"
else:
title = f"G{group_index + 1} - SHARE {share_index + 1}"
# Showing words, asking for write down confirmation and preparing for check
# until user accepts everything.
while True:
await interact(
ctx,
RustLayout(
trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"]
title=title,
share_words=share_words, # type: ignore [No parameter named "share_words"]
)
),
"backup_words",
ButtonRequestType.ResetDevice,
)
if share_index is None:
check_title = "CHECK BACKUP"
elif group_index is None:
check_title = f"CHECK SHARE #{share_index + 1}"
else:
check_title = f"GROUP {group_index + 1} - SHARE {share_index + 1}"
if await get_bool(
ctx,
"backup_words",
check_title,
None,
"Select correct word for each position.",
verb_cancel="SEE AGAIN",
verb="BEGIN",
br_code=ButtonRequestType.ResetDevice,
):
# All went well, we can break the loop.
break
async def select_word(
ctx: GenericContext,
words: Sequence[str],
share_index: int | None,
checked_index: int,
count: int,
group_index: int | None = None,
) -> str:
from trezor.strings import format_ordinal
# It may happen (with a very low probability)
# that there will be less than three unique words to choose from.
# In that case, duplicating the last word to make it three.
words = list(words)
while len(words) < 3:
words.append(words[-1])
result = await ctx.wait(
RustLayout(
trezorui2.select_word(
title="",
description=f"SELECT {format_ordinal(checked_index + 1).upper()} WORD",
words=(words[0].lower(), words[1].lower(), words[2].lower()),
)
)
)
if __debug__ and isinstance(result, str):
return result
assert isinstance(result, int) and 0 <= result <= 2
return words[result]
async def slip39_show_checklist(
ctx: GenericContext, step: int, backup_type: BackupType
) -> None:
from trezor.enums import BackupType
assert backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced)
items = (
(
"Number of shares",
"Set threshold",
"Write down and check all shares",
)
if backup_type == BackupType.Slip39_Basic
else (
"Number of groups",
"Number of shares",
"Set sizes and thresholds",
)
)
result = await interact(
ctx,
RustLayout(
trezorui2.show_checklist(
title="BACKUP CHECKLIST",
button="CONTINUE",
active=step,
items=items,
)
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def _prompt_number(
ctx: GenericContext,
title: str,
count: int,
min_count: int,
max_count: int,
br_name: str,
) -> int:
num_input = RustLayout(
trezorui2.request_number(
title=title.upper(),
count=count,
min_count=min_count,
max_count=max_count,
)
)
result = await interact(
ctx,
num_input,
br_name,
ButtonRequestType.ResetDevice,
)
return int(result)
async def slip39_prompt_threshold(
ctx: GenericContext, num_of_shares: int, group_id: int | None = None
) -> int:
await confirm_action(
ctx,
"slip39_prompt_threshold",
"Threshold",
description="= number of shares needed for recovery",
verb="BEGIN",
verb_cancel=None,
)
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 advanced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
if group_id is not None:
title = f"THRESHOLD - GROUP {group_id + 1}"
else:
title = "SET THRESHOLD"
return await _prompt_number(
ctx,
title,
count,
min_count,
max_count,
"slip39_threshold",
)
async def slip39_prompt_number_of_shares(
ctx: GenericContext, group_id: int | None = None
) -> int:
await confirm_action(
ctx,
"slip39_shares",
"Number of shares",
description="= total number of unique word lists used for wallet backup",
verb="BEGIN",
verb_cancel=None,
)
count = 5
min_count = 1
max_count = 16
if group_id is not None:
title = f"# SHARES - GROUP {group_id + 1}"
else:
title = "NUMBER OF SHARES"
return await _prompt_number(
ctx,
title,
count,
min_count,
max_count,
"slip39_shares",
)
async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int:
count = 5
min_count = 2
max_count = 16
return await _prompt_number(
ctx,
"NUMBER OF GROUPS",
count,
min_count,
max_count,
"slip39_groups",
)
async def slip39_advanced_prompt_group_threshold(
ctx: GenericContext, num_of_groups: int
) -> int:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
return await _prompt_number(
ctx,
"GROUP THRESHOLD",
count,
min_count,
max_count,
"slip39_group_threshold",
)
async def show_warning_backup(ctx: GenericContext, slip39: bool) -> None:
await confirm_action(
ctx,
"backup_warning",
"SHAMIR BACKUP" if slip39 else "WALLET BACKUP",
description="You can use your backup to recover your wallet at any time.",
verb="HOLD TO BEGIN",
hold=True,
br_code=ButtonRequestType.ResetDevice,
)
async def show_success_backup(ctx: GenericContext) -> None:
from . import confirm_action
await confirm_action(
ctx,
"success_backup",
"BACKUP IS DONE",
description="Keep it safe!",
verb="CONTINUE",
verb_cancel=None,
br_code=ButtonRequestType.Success,
)

@ -503,6 +503,7 @@ async def confirm_output(
amount: str,
title: str | None = None,
hold: bool = False,
index: int | None = None,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
address_label: str | None = None,
output_index: int | None = None,

Loading…
Cancel
Save