1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 22:38:08 +00:00

feat(core): unify RustLayout, implement single global layout

This commit is contained in:
matejcik 2024-07-04 09:32:59 +02:00 committed by matejcik
parent df368413c6
commit 0e8dcbb498
50 changed files with 2804 additions and 4072 deletions

View File

@ -51,7 +51,7 @@ message DebugLinkDecision {
optional uint32 x = 4; // touch X coordinate
optional uint32 y = 5; // touch Y coordinate
optional bool wait = 6; // wait for layout change
optional bool wait = 6 [deprecated=true]; // wait for layout change
optional uint32 hold_ms = 7; // touch hold duration
optional DebugPhysicalButton physical_button = 8; // physical button press
}
@ -61,6 +61,7 @@ message DebugLinkDecision {
* @end
*/
message DebugLinkLayout {
option deprecated = true;
repeated string tokens = 1;
}
@ -89,9 +90,26 @@ message DebugLinkRecordScreen {
* @next DebugLinkState
*/
message DebugLinkGetState {
optional bool wait_word_list = 1; // Trezor T only - wait until mnemonic words are shown
optional bool wait_word_pos = 2; // Trezor T only - wait until reset word position is requested
optional bool wait_layout = 3; // wait until current layout changes
/// Wait behavior of the call.
enum DebugWaitType {
/// Respond immediately. If no layout is currently displayed, the layout
/// response will be empty.
IMMEDIATE = 0;
/// Wait for next layout. If a layout is displayed, waits for it to change.
/// If no layout is displayed, waits for one to come up.
NEXT_LAYOUT = 1;
/// Return current layout. If no layout is currently displayed, waits for
/// one to come up.
CURRENT_LAYOUT = 2;
}
// Trezor T < 2.6.0 only - wait until mnemonic words are shown
optional bool wait_word_list = 1 [deprecated=true];
// Trezor T < 2.6.0 only - wait until reset word position is requested
optional bool wait_word_pos = 2 [deprecated=true];
// trezor-core only - wait until current layout changes
// changed in 2.6.4: multiple wait types instead of true/false.
optional DebugWaitType wait_layout = 3 [default=IMMEDIATE];
}
/**
@ -192,6 +210,7 @@ message DebugLinkEraseSdCard {
* @next Success
*/
message DebugLinkWatchLayout {
option deprecated = true;
optional bool watch = 1; // if true, start watching layout.
// if false, stop.
}
@ -203,6 +222,7 @@ message DebugLinkWatchLayout {
* @next Success
*/
message DebugLinkResetDebugEvents {
option deprecated = true;
}

View File

@ -311,17 +311,11 @@ impl SwipeFlow {
}
}
/// ObjComponent implementation for SwipeFlow.
/// Layout implementation for SwipeFlow.
///
/// Instead of using the generic `impl ObjComponent for ComponentMsgObj`, we
/// provide our own short-circuit implementation for `SwipeFlow`. This way we
/// can completely avoid implementing `Component`. That also allows us to pass
/// around concrete Renderers instead of having to conform to `Component`'s
/// not-object-safe interface.
///
/// This implementation relies on the fact that swipe components always return
/// `FlowMsg` as their `Component::Msg` (provided by `impl FlowComponentTrait`
/// earlier in this file).
/// This way we can completely avoid implementing `Component`. That also allows
/// us to pass around concrete Renderers instead of having to conform to
/// `Component`'s not-object-safe interface.
impl Layout<Result<Obj, Error>> for SwipeFlow {
fn place(&mut self) {
for elem in self.store.iter_mut() {

View File

@ -12,7 +12,6 @@ pub enum LayoutState {
}
pub trait Layout<T> {
//fn attach(&mut self, ctx: &mut EventCtx, attach_type: AttachType);
fn place(&mut self);
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<LayoutState>;
fn value(&self) -> Option<&T>;

View File

@ -1662,7 +1662,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Attach a timer setter function.
///
/// The layout object can call the timer setter with two arguments,
/// `token` and `duration`. When `duration` elapses, the layout object
/// `token` and `duration_ms`. When `duration_ms` elapses, the layout object
/// expects a callback to `self.timer(token)`.
/// """
///

View File

@ -1091,10 +1091,10 @@ class LayoutObj(Generic[T]):
"""Representation of a Rust-based layout object.
see `trezor::ui::layout::obj::LayoutObj`.
"""
def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None:
def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> LayoutState | None:
"""Attach a timer setter function.
The layout object can call the timer setter with two arguments,
`token` and `duration`. When `duration` elapses, the layout object
`token` and `duration_ms`. When `duration_ms` elapses, the layout object
expects a callback to `self.timer(token)`.
"""
if utils.USE_TOUCH:
@ -1136,7 +1136,6 @@ class LayoutObj(Generic[T]):
"""Return (code, type) of button request made during the last event or timer pass."""
def get_transition_out(self) -> AttachType:
"""Return the transition type."""
def return_value(self) -> T:
"""Retrieve the return value of the layout object."""
def __del__(self) -> None:

View File

@ -107,6 +107,8 @@ trezor.enums.DebugPhysicalButton
import trezor.enums.DebugPhysicalButton
trezor.enums.DebugSwipeDirection
import trezor.enums.DebugSwipeDirection
trezor.enums.DebugWaitType
import trezor.enums.DebugWaitType
trezor.enums.DecredStakingSpendType
import trezor.enums.DecredStakingSpendType
trezor.enums.FailureType
@ -167,8 +169,6 @@ trezor.ui.layouts.mercury
import trezor.ui.layouts.mercury
trezor.ui.layouts.mercury.fido
import trezor.ui.layouts.mercury.fido
trezor.ui.layouts.mercury.homescreen
import trezor.ui.layouts.mercury.homescreen
trezor.ui.layouts.mercury.recovery
import trezor.ui.layouts.mercury.recovery
trezor.ui.layouts.mercury.reset
@ -183,8 +183,6 @@ 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.recovery
import trezor.ui.layouts.tr.recovery
trezor.ui.layouts.tr.reset
@ -193,8 +191,6 @@ trezor.ui.layouts.tt
import trezor.ui.layouts.tt
trezor.ui.layouts.tt.fido
import trezor.ui.layouts.tt.fido
trezor.ui.layouts.tt.homescreen
import trezor.ui.layouts.tt.homescreen
trezor.ui.layouts.tt.recovery
import trezor.ui.layouts.tt.recovery
trezor.ui.layouts.tt.reset

View File

@ -20,7 +20,6 @@ from ..keychain import address_n_to_name
if TYPE_CHECKING:
from trezor.enums import AmountUnit
from trezor.messages import TxAckPaymentRequest, TxOutput
from trezor.ui.layouts import LayoutType
from apps.common.coininfo import CoinInfo
from apps.common.paths import Bip32Path
@ -73,7 +72,7 @@ async def confirm_output(
assert data is not None
if omni.is_valid(data):
# OMNI transaction
layout: LayoutType = confirm_metadata(
layout = confirm_metadata(
"omni_transaction",
"OMNI transaction",
omni.parse(data),

View File

@ -122,10 +122,6 @@ class Progress:
self.progress_layout = progress_layout(text)
def report(self) -> None:
from trezor import utils
if utils.DISABLE_ANIMATION:
return
p = int(1000 * self.progress / self.steps)
self.progress_layout.report(p)

View File

@ -7,7 +7,7 @@ from . import backup_types
if TYPE_CHECKING:
from trezor.enums import BackupType
from trezor.ui.layouts.common import ProgressLayout
from trezor.ui import ProgressLayout
def get() -> tuple[bytes | None, BackupType]:

View File

@ -1,12 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR, io, utils, wire
from trezor.ui.layouts import confirm_action, show_error_and_raise
from trezor.utils import sd_hotswap_enabled
if TYPE_CHECKING:
from trezor.ui.layouts.common import ProgressLayout
class SdCardUnavailable(wire.ProcessError):
pass
@ -102,6 +97,7 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None:
mounted.
"""
from trezor import sdcard
from trezor.ui.layouts.progress import progress
while not sdcard.is_present():
await _confirm_retry_insert_card()
@ -125,11 +121,12 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None:
# Proceed to formatting. Failure is caught by the outside OSError handler
with sdcard.filesystem(mounted=False):
_start_progress()
fatfs.mkfs(_render_progress)
progress_obj = progress()
progress_obj.start()
fatfs.mkfs(progress_obj.report)
fatfs.mount()
fatfs.setlabel("TREZOR")
_finish_progress()
progress_obj.stop()
# format and mount succeeded
return
@ -157,30 +154,3 @@ async def request_sd_salt() -> bytearray | None:
# In either case, there is no good way to recover. If the user clicks Retry,
# we will try again.
await confirm_retry_sd()
_progress_obj: ProgressLayout | None = None
def _start_progress() -> None:
from trezor import workflow
from trezor.ui.layouts.progress import progress
global _progress_obj
if not utils.DISABLE_ANIMATION:
# Because we are drawing to the screen manually, without a layout, we
# should make sure that no other layout is running.
workflow.close_others()
_progress_obj = progress()
def _render_progress(progress: int) -> None:
global _progress_obj
if _progress_obj is not None:
_progress_obj.report(progress)
def _finish_progress() -> None:
global _progress_obj
_progress_obj = None

View File

@ -4,20 +4,21 @@ if not __debug__:
halt("debug mode inactive")
if __debug__:
import utime
from micropython import const
from typing import TYPE_CHECKING
import trezorui2
from storage import debug as storage
from storage.debug import debug_events
from trezor import log, loop, utils, wire
from trezor.enums import MessageType
from trezor.messages import DebugLinkLayout, Success
from trezor import io, log, loop, ui, utils, wire, workflow
from trezor.enums import DebugWaitType, MessageType
from trezor.messages import Success
from trezor.ui import display
from trezor.wire import context
from apps import workflow_handlers
if TYPE_CHECKING:
from typing import Any, Awaitable, Callable
from trezor.enums import DebugButton, DebugPhysicalButton, DebugSwipeDirection
from trezor.messages import (
DebugLinkDecision,
DebugLinkEraseSdCard,
@ -25,33 +26,22 @@ if __debug__:
DebugLinkOptigaSetSecMax,
DebugLinkRecordScreen,
DebugLinkReseedRandom,
DebugLinkResetDebugEvents,
DebugLinkState,
DebugLinkWatchLayout,
)
from trezor.ui import Layout
from trezor.wire import WireInterface, context
swipe_chan = loop.chan()
result_chan = loop.chan()
button_chan = loop.chan()
click_chan = loop.chan()
swipe_signal = swipe_chan.take
result_signal = result_chan.take
button_signal = button_chan.take
click_signal = click_chan.take
Handler = Callable[[Any], Awaitable[Any]]
debuglink_decision_chan = loop.chan()
layout_change_chan = loop.chan()
layout_change_box = loop.mailbox()
DEBUG_CONTEXT: context.Context | None = None
LAYOUT_WATCHER_NONE = 0
LAYOUT_WATCHER_STATE = 1
LAYOUT_WATCHER_LAYOUT = 2
REFRESH_INDEX = 0
_DEADLOCK_SLEEP_MS = const(3000)
_DEADLOCK_DETECT_SLEEP = loop.sleep(_DEADLOCK_SLEEP_MS)
def screenshot() -> bool:
if storage.save_screen:
# Starting with "refresh00", allowing for 100 emulator restarts
@ -62,173 +52,242 @@ if __debug__:
return True
return False
def notify_layout_change(layout: Layout, event_id: int | None = None) -> None:
layout.read_content_into(storage.current_content_tokens)
if storage.watch_layout_changes or layout_change_chan.takers:
payload = (event_id, storage.current_content_tokens)
layout_change_chan.publish(payload)
def notify_layout_change(layout: Layout | None) -> None:
layout_change_box.put(layout, replace=True)
async def _dispatch_debuglink_decision(
event_id: int | None, msg: DebugLinkDecision
def layout_is_ready() -> bool:
layout = ui.CURRENT_LAYOUT
return isinstance(layout, ui.Layout) and layout.is_layout_attached()
def wait_until_layout_is_running(timeout: int | None = _DEADLOCK_SLEEP_MS) -> Awaitable[None]: # type: ignore [awaitable-return-type]
start = utime.ticks_ms()
layout_change_box.clear()
while not layout_is_ready():
yield layout_change_box # type: ignore [awaitable-return-type]
now = utime.ticks_ms()
if timeout and utime.ticks_diff(now, start) > timeout:
raise wire.FirmwareError(
"layout deadlock detected (did you send a ButtonAck?)"
)
async def return_layout_change(
ctx: wire.context.Context, detect_deadlock: bool = False
) -> None:
# set up the wait
storage.layout_watcher = True
# wait for layout change
while True:
if not detect_deadlock or not layout_change_box.is_empty():
# short-circuit if there is a result already waiting
next_layout = await layout_change_box
else:
next_layout = await loop.race(layout_change_box, _DEADLOCK_DETECT_SLEEP)
if isinstance(next_layout, int):
# sleep result from the deadlock detector
raise wire.FirmwareError("layout deadlock detected")
if next_layout is None:
# we are reading the "layout ended" event, spin once more to grab the
# "new layout started" event
continue
if layout_is_ready():
break
assert ui.CURRENT_LAYOUT is next_layout
# send the message and reset the wait
storage.layout_watcher = False
await ctx.write(_state())
async def _layout_click(x: int, y: int, hold_ms: int = 0) -> None:
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_START, x, y
)
if hold_ms:
await loop.sleep(hold_ms)
workflow.idle_timer.touch()
if not layout_is_ready():
return
ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_END, x, y
)
async def _layout_press_button(
debug_btn: DebugPhysicalButton, hold_ms: int = 0
) -> None:
from trezor.enums import DebugPhysicalButton
buttons = []
if debug_btn == DebugPhysicalButton.LEFT_BTN:
buttons.append(io.BUTTON_LEFT)
elif debug_btn == DebugPhysicalButton.RIGHT_BTN:
buttons.append(io.BUTTON_RIGHT)
elif debug_btn == DebugPhysicalButton.MIDDLE_BTN:
buttons.append(io.BUTTON_LEFT)
buttons.append(io.BUTTON_RIGHT)
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
for btn in buttons:
ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_PRESSED, btn
)
if hold_ms:
await loop.sleep(hold_ms)
workflow.idle_timer.touch()
if not layout_is_ready():
return
for btn in buttons:
ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_RELEASED, btn
)
if utils.USE_TOUCH:
async def _layout_swipe(direction: DebugSwipeDirection) -> None: # type: ignore [obscured by a declaration of the same name]
from trezor.enums import DebugSwipeDirection
orig_x = orig_y = 120
off_x, off_y = {
DebugSwipeDirection.UP: (0, -30),
DebugSwipeDirection.DOWN: (0, 30),
DebugSwipeDirection.LEFT: (-30, 0),
DebugSwipeDirection.RIGHT: (30, 0),
}[direction]
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
for event, x, y in (
(io.TOUCH_START, orig_x, orig_y),
(io.TOUCH_MOVE, orig_x + 1 * off_x, orig_y + 1 * off_y),
(io.TOUCH_END, orig_x + 2 * off_x, orig_y + 2 * off_y),
):
ui.CURRENT_LAYOUT._event(
ui.CURRENT_LAYOUT.layout.touch_event, event, x, y
)
elif utils.USE_BUTTON:
def _layout_swipe(direction: DebugSwipeDirection) -> Awaitable[None]:
from trezor.enums import DebugPhysicalButton, DebugSwipeDirection
if direction == DebugSwipeDirection.UP:
button = DebugPhysicalButton.RIGHT_BTN
elif direction == DebugSwipeDirection.DOWN:
button = DebugPhysicalButton.LEFT_BTN
else:
raise RuntimeError # unsupported swipe direction on TR
return _layout_press_button(button)
else:
raise RuntimeError # No way to swipe with no buttons and no touches
async def _layout_event(button: DebugButton) -> None:
from trezor.enums import DebugButton
if msg.button is not None:
if msg.button == DebugButton.NO:
await result_chan.put((event_id, trezorui2.CANCELLED))
elif msg.button == DebugButton.YES:
await result_chan.put((event_id, trezorui2.CONFIRMED))
elif msg.button == DebugButton.INFO:
await result_chan.put((event_id, trezorui2.INFO))
else:
raise RuntimeError(f"Invalid msg.button - {msg.button}")
elif msg.input is not None:
await result_chan.put((event_id, msg.input))
elif msg.swipe is not None:
await swipe_chan.put((event_id, msg.swipe))
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
if button == DebugButton.NO:
ui.CURRENT_LAYOUT._emit_message(trezorui2.CANCELLED)
elif button == DebugButton.YES:
ui.CURRENT_LAYOUT._emit_message(trezorui2.CONFIRMED)
elif button == DebugButton.INFO:
ui.CURRENT_LAYOUT._emit_message(trezorui2.INFO)
else:
# Sanity check. The message will be visible in terminal.
raise RuntimeError("Invalid DebugLinkDecision message")
raise RuntimeError("Invalid DebugButton")
async def debuglink_decision_dispatcher() -> None:
while True:
event_id, msg = await debuglink_decision_chan.take()
await _dispatch_debuglink_decision(event_id, msg)
async def get_layout_change_content() -> list[str]:
awaited_event_id = debug_events.awaited_event
last_result_id = debug_events.last_result
if awaited_event_id is not None and awaited_event_id == last_result_id:
# We are awaiting the event that just happened - return current state
return storage.current_content_tokens
while True:
event_id, content = await layout_change_chan.take()
if awaited_event_id is None or event_id is None:
# Not waiting for anything or event does not have ID
break
elif event_id == awaited_event_id:
# We found what we were waiting for
debug_events.awaited_event = None
break
elif event_id > awaited_event_id:
# Sanity check
pass
# TODO: find out why this sometimes happens on TR when running tests with
# "physical" emulator (./emu.py)
# raise RuntimeError(
# f"Waiting for event that already happened - {event_id} > {awaited_event_id}"
# )
if awaited_event_id is not None:
# Updating last result
debug_events.last_result = awaited_event_id
return content
async def return_layout_change() -> None: # type: ignore [Return type of async generator]
content_tokens = await get_layout_change_content()
# spin for a bit until DEBUG_CONTEXT becomes available
while not isinstance(DEBUG_CONTEXT, context.Context):
yield # type: ignore [Return type of async generator]
if storage.layout_watcher is LAYOUT_WATCHER_LAYOUT:
await DEBUG_CONTEXT.write(DebugLinkLayout(tokens=content_tokens))
else:
from trezor.messages import DebugLinkState
await DEBUG_CONTEXT.write(DebugLinkState(tokens=content_tokens))
storage.layout_watcher = LAYOUT_WATCHER_NONE
async def dispatch_DebugLinkWatchLayout(msg: DebugLinkWatchLayout) -> Success:
from trezor import ui
layout_change_chan.putters.clear()
if msg.watch:
await ui.wait_until_layout_is_running()
storage.watch_layout_changes = bool(msg.watch)
log.debug(__name__, "Watch layout changes: %s", storage.watch_layout_changes)
return Success()
async def dispatch_DebugLinkResetDebugEvents(
msg: DebugLinkResetDebugEvents,
) -> Success:
# Resetting the debug events makes sure that the previous
# events/layouts are not mixed with the new ones.
storage.reset_debug_events()
return Success()
async def dispatch_DebugLinkDecision(msg: DebugLinkDecision) -> None:
from trezor import workflow
async def dispatch_DebugLinkDecision(
msg: DebugLinkDecision,
) -> DebugLinkState | None:
from trezor import ui, workflow
workflow.idle_timer.touch()
if debuglink_decision_chan.putters:
log.warning(__name__, "DebugLinkDecision queue is not empty")
x = msg.x # local_cache_attribute
y = msg.y # local_cache_attribute
# Incrementing the counter for last events so we know what to await
debug_events.last_event += 1
await wait_until_layout_is_running()
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
layout_change_box.clear()
# Touchscreen devices click on specific coordinates, with possible hold
if (
x is not None
and y is not None
and utils.INTERNAL_MODEL # pylint: disable=internal-model-tuple-comparison
in ("T2T1", "T3T1", "D001")
):
click_chan.publish((debug_events.last_event, x, y, msg.hold_ms))
# Button devices press specific button
elif (
msg.physical_button is not None
and utils.INTERNAL_MODEL # pylint: disable=internal-model-tuple-comparison
in ("T2B1", "T3B1")
):
button_chan.publish(
(debug_events.last_event, msg.physical_button, msg.hold_ms)
)
else:
# Will get picked up by _dispatch_debuglink_decision eventually
debuglink_decision_chan.publish((debug_events.last_event, msg))
try:
# click on specific coordinates, with possible hold
if x is not None and y is not None:
await _layout_click(x, y, msg.hold_ms or 0)
# press specific button
elif msg.physical_button is not None:
await _layout_press_button(msg.physical_button, msg.hold_ms or 0)
elif msg.swipe is not None:
await _layout_swipe(msg.swipe)
elif msg.button is not None:
await _layout_event(msg.button)
elif msg.input is not None:
ui.CURRENT_LAYOUT._emit_message(msg.input)
else:
raise RuntimeError("Invalid DebugLinkDecision message")
if msg.wait:
# We wait for all the previously sent events
debug_events.awaited_event = debug_events.last_event
storage.layout_watcher = LAYOUT_WATCHER_LAYOUT
loop.schedule(return_layout_change())
except ui.Shutdown:
# Shutdown should be raised if the layout is supposed to stop after
# processing the event. In that case, we need to yield to give the layout
# callers time to finish their jobs. We want to make sure that the handling
# does not continue until the event is truly processed.
result = await layout_change_box
assert result is None
async def dispatch_DebugLinkGetState(
msg: DebugLinkGetState,
) -> DebugLinkState | None:
# If no exception was raised, the layout did not shut down. That means that it
# just updated itself. The update is already live for the caller to retrieve.
def _state() -> DebugLinkState:
from trezor.messages import DebugLinkState
from apps.common import mnemonic, passphrase
m = DebugLinkState()
m.mnemonic_secret = mnemonic.get_secret()
m.mnemonic_type = mnemonic.get_type()
m.passphrase_protection = passphrase.is_enabled()
m.reset_entropy = storage.reset_internal_entropy
tokens = []
if msg.wait_layout:
if not storage.watch_layout_changes:
raise wire.ProcessError("Layout is not watched")
storage.layout_watcher = LAYOUT_WATCHER_STATE
# We wait for the last previously sent event to finish
debug_events.awaited_event = debug_events.last_event
loop.schedule(return_layout_change())
return None
def callback(*args: str) -> None:
tokens.extend(args)
if ui.CURRENT_LAYOUT is not None:
ui.CURRENT_LAYOUT.layout.trace(callback)
return DebugLinkState(
mnemonic_secret=mnemonic.get_secret(),
mnemonic_type=mnemonic.get_type(),
passphrase_protection=passphrase.is_enabled(),
reset_entropy=storage.reset_internal_entropy,
tokens=tokens,
)
async def dispatch_DebugLinkGetState(
msg: DebugLinkGetState,
) -> DebugLinkState | None:
if msg.wait_layout == DebugWaitType.IMMEDIATE:
return _state()
assert DEBUG_CONTEXT is not None
if msg.wait_layout == DebugWaitType.NEXT_LAYOUT:
layout_change_box.clear()
return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=False)
# default behavior: msg.wait_layout == DebugWaitType.CURRENT_LAYOUT
if not layout_is_ready():
return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=True)
else:
m.tokens = storage.current_content_tokens
return m
return _state()
async def dispatch_DebugLinkRecordScreen(msg: DebugLinkRecordScreen) -> Success:
if msg.target_directory:
# Ensure we consistently start at a layout, instead of randomly sometimes
# hitting the pause between layouts and rendering the "upcoming" one.
await wait_until_layout_is_running()
# In case emulator is restarted but we still want to record screenshots
# into the same directory as before, we need to increment the refresh index,
# so that the screenshots are not overwritten.
@ -236,6 +295,14 @@ if __debug__:
REFRESH_INDEX = msg.refresh_index
storage.save_screen_directory = msg.target_directory
storage.save_screen = True
# force repaint current layout, in order to take an initial screenshot
# (doing it this way also clears the red square, because the repaint is
# happening with screenshotting already enabled)
assert isinstance(ui.CURRENT_LAYOUT, ui.Layout)
ui.CURRENT_LAYOUT.request_complete_repaint()
ui.CURRENT_LAYOUT._paint()
else:
storage.save_screen = False
display.clear_save() # clear C buffers
@ -271,26 +338,6 @@ if __debug__:
sdcard.power_off()
return Success()
def boot() -> None:
register = workflow_handlers.register # local_cache_attribute
register(MessageType.DebugLinkDecision, dispatch_DebugLinkDecision) # type: ignore [Argument of type "(msg: DebugLinkDecision) -> Coroutine[Any, Any, None]" cannot be assigned to parameter "handler" of type "Handler[Msg@register]" in function "register"]
register(MessageType.DebugLinkGetState, dispatch_DebugLinkGetState) # type: ignore [Argument of type "(msg: DebugLinkGetState) -> Coroutine[Any, Any, DebugLinkState | None]" cannot be assigned to parameter "handler" of type "Handler[Msg@register]" in function "register"]
register(MessageType.DebugLinkReseedRandom, dispatch_DebugLinkReseedRandom)
register(MessageType.DebugLinkRecordScreen, dispatch_DebugLinkRecordScreen)
register(MessageType.DebugLinkEraseSdCard, dispatch_DebugLinkEraseSdCard)
register(MessageType.DebugLinkWatchLayout, dispatch_DebugLinkWatchLayout)
register(
MessageType.DebugLinkResetDebugEvents, dispatch_DebugLinkResetDebugEvents
)
register(
MessageType.DebugLinkOptigaSetSecMax, dispatch_DebugLinkOptigaSetSecMax
)
loop.schedule(debuglink_decision_dispatcher())
if storage.layout_watcher is not LAYOUT_WATCHER_NONE:
loop.schedule(return_layout_change())
async def dispatch_DebugLinkOptigaSetSecMax(
msg: DebugLinkOptigaSetSecMax,
) -> Success:
@ -301,3 +348,89 @@ if __debug__:
return Success()
else:
raise wire.UnexpectedMessage("Optiga not supported")
async def _no_op(_msg: Any) -> Success:
return Success()
WIRE_BUFFER_DEBUG = bytearray(1024)
async def handle_session(iface: WireInterface) -> None:
from trezor import protobuf, wire
from trezor.wire import codec_v1, context
global DEBUG_CONTEXT
DEBUG_CONTEXT = ctx = context.Context(iface, 0, WIRE_BUFFER_DEBUG)
if storage.layout_watcher:
try:
await return_layout_change(ctx)
except Exception as e:
log.exception(__name__, e)
while True:
try:
try:
msg = await ctx.read_from_wire()
except codec_v1.CodecError as exc:
log.exception(__name__, exc)
await ctx.write(wire.failure(exc))
continue
req_type = None
try:
req_type = protobuf.type_for_wire(msg.type)
msg_type = req_type.MESSAGE_NAME
except Exception:
msg_type = f"{msg.type} - unknown message type"
log.debug(
__name__,
"%s:%x receive: <%s>",
ctx.iface.iface_num(),
ctx.sid,
msg_type,
)
if msg.type not in WORKFLOW_HANDLERS:
await ctx.write(wire.unexpected_message())
continue
elif req_type is None:
# Message type is in workflow handlers but not in protobuf
# definitions. This indicates a deprecated message.
# We put a no-op handler for those messages.
# XXX return a Failure here?
await ctx.write(Success())
continue
req_msg = wire.wrap_protobuf_load(msg.data, req_type)
try:
res_msg = await WORKFLOW_HANDLERS[msg.type](req_msg)
except Exception as exc:
# Log and ignore, never die.
log.exception(__name__, exc)
res_msg = wire.failure(exc)
if res_msg is not None:
await ctx.write(res_msg)
except Exception as exc:
# Log and try again. This should only happen for USB errors and we
# try to stay robust in such case.
log.exception(__name__, exc)
WORKFLOW_HANDLERS: dict[int, Handler] = {
MessageType.DebugLinkDecision: dispatch_DebugLinkDecision,
MessageType.DebugLinkGetState: dispatch_DebugLinkGetState,
MessageType.DebugLinkReseedRandom: dispatch_DebugLinkReseedRandom,
MessageType.DebugLinkRecordScreen: dispatch_DebugLinkRecordScreen,
MessageType.DebugLinkEraseSdCard: dispatch_DebugLinkEraseSdCard,
MessageType.DebugLinkOptigaSetSecMax: dispatch_DebugLinkOptigaSetSecMax,
MessageType.DebugLinkWatchLayout: _no_op,
MessageType.DebugLinkResetDebugEvents: _no_op,
}
def boot() -> None:
import usb
loop.schedule(handle_session(usb.iface_debug))

View File

@ -20,7 +20,6 @@ if TYPE_CHECKING:
EthereumTokenInfo,
EthereumTxAck,
)
from trezor.ui.layouts.common import ProgressLayout
from apps.common.keychain import Keychain
@ -40,7 +39,9 @@ async def sign_tx(
keychain: Keychain,
defs: Definitions,
) -> EthereumTxRequest:
from trezor import TR
from trezor.crypto.hashlib import sha3_256
from trezor.ui.layouts.progress import progress
from trezor.utils import HashWriter
from apps.common import paths
@ -69,9 +70,8 @@ async def sign_tx(
)
await confirm_tx_data(msg, defs, address_bytes, maximum_fee, fee_items, data_total)
_start_progress()
_render_progress(30)
progress_obj = progress(title=TR.progress__signing_transaction)
progress_obj.report(30)
# sign
data = bytearray()
@ -95,7 +95,7 @@ async def sign_tx(
rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, data)
sha.extend(data)
_render_progress(60)
progress_obj.report(60)
while data_left > 0:
resp = await send_request_chunk(data_left)
@ -110,7 +110,7 @@ async def sign_tx(
digest = sha.get_digest()
result = _sign_digest(msg, keychain, digest)
_finish_progress()
progress_obj.stop()
return result
@ -396,30 +396,3 @@ async def _handle_staking_tx_claim(
await require_confirm_claim(
staking_addr, msg.address_n, maximum_fee, fee_items, network, chunkify
)
_progress_obj: ProgressLayout | None = None
def _start_progress() -> None:
from trezor import TR, workflow
from trezor.ui.layouts.progress import progress
global _progress_obj
if not utils.DISABLE_ANIMATION:
# Because we are drawing to the screen manually, without a layout, we
# should make sure that no other layout is running.
workflow.close_others()
_progress_obj = progress(title=TR.progress__signing_transaction)
def _render_progress(progress: int) -> None:
global _progress_obj
if _progress_obj is not None:
_progress_obj.report(progress)
def _finish_progress() -> None:
global _progress_obj
_progress_obj = None

View File

@ -14,7 +14,7 @@ from apps.common.authorization import is_set_any_session
async def busyscreen() -> None:
obj = Busyscreen(busy_expiry_ms())
try:
await obj
await obj.get_result()
finally:
obj.__del__()
@ -53,7 +53,7 @@ async def homescreen() -> None:
hold_to_lock=config.has_pin(),
)
try:
await obj
await obj.get_result()
finally:
obj.__del__()
@ -72,7 +72,7 @@ async def _lockscreen(screensaver: bool = False) -> None:
coinjoin_authorized=is_set_any_session(MessageType.AuthorizeCoinJoin),
)
try:
await obj
await obj.get_result()
finally:
obj.__del__()
# Otherwise proceed directly to unlock() call. If the device is already unlocked,

View File

@ -8,7 +8,7 @@ if TYPE_CHECKING:
from typing import Callable
from trezor.messages import ChangeLanguage, Success
from trezor.ui.layouts.common import ProgressLayout
from trezor.ui import ProgressLayout
_CHUNK_SIZE = const(1024)

View File

@ -23,12 +23,11 @@ if TYPE_CHECKING:
async def request_mnemonic(
word_count: int, backup_type: BackupType | None
) -> str | None:
from trezor.ui.layouts.common import button_request
from trezor.ui.layouts.recovery import request_word
from . import word_validity
await button_request("mnemonic", code=ButtonRequestType.MnemonicInput)
send_button_request = True
# Pre-allocate the list to enable going back and overwriting words.
words: list[str] = [""] * word_count
@ -43,8 +42,10 @@ async def request_mnemonic(
i,
word_count,
is_slip39=backup_types.is_slip39_word_count(word_count),
send_button_request=send_button_request,
prefill_word=words[i],
)
send_button_request = False
if not word:
# User has decided to go back

View File

@ -2,9 +2,6 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from trezor.messages import FirmwareHash, GetFirmwareHash
from trezor.ui.layouts.common import ProgressLayout
_progress_obj: ProgressLayout | None = None
async def get_firmware_hash(msg: GetFirmwareHash) -> FirmwareHash:
@ -14,20 +11,14 @@ async def get_firmware_hash(msg: GetFirmwareHash) -> FirmwareHash:
from trezor.utils import firmware_hash
workflow.close_others()
global _progress_obj
_progress_obj = progress()
progress_obj = progress()
def report(progress: int, total: int) -> None:
progress_obj.report(1000 * progress // total)
try:
hash = firmware_hash(msg.challenge, _render_progress)
hash = firmware_hash(msg.challenge, report)
except ValueError as e:
raise wire.DataError(str(e))
finally:
_progress_obj = None
return FirmwareHash(hash=hash)
def _render_progress(progress: int, total: int) -> None:
global _progress_obj
if _progress_obj is not None:
_progress_obj.report(1000 * progress // total)

View File

@ -8,7 +8,8 @@ import storage.device as storage_device
from trezor import TR, config, io, log, loop, utils, wire, workflow
from trezor.crypto import hashlib
from trezor.crypto.curve import nist256p1
from trezor.ui.layouts import show_error_popup
from trezor.ui import Layout
from trezor.ui.layouts import error_popup
from apps.base import set_homescreen
from apps.common import cbor
@ -615,16 +616,36 @@ async def _confirm_fido(title: str, credential: Credential) -> bool:
return False
async def _show_error_popup(
title: str,
description: str,
subtitle: str | None = None,
description_param: str = "",
*,
button: str = "",
timeout_ms: int = 0,
) -> None:
popup = error_popup(
title,
description,
subtitle,
description_param,
button=button,
timeout_ms=timeout_ms,
)
await Layout(popup).get_result()
async def _confirm_bogus_app(title: str) -> None:
if _last_auth_valid:
await show_error_popup(
await _show_error_popup(
title,
TR.fido__device_already_registered,
TR.fido__already_registered,
timeout_ms=_POPUP_TIMEOUT_MS,
)
else:
await show_error_popup(
await _show_error_popup(
title,
TR.fido__device_not_registered,
TR.fido__not_registered,
@ -841,7 +862,7 @@ class Fido2ConfirmExcluded(Fido2ConfirmMakeCredential):
await send_cmd(cmd, self.iface)
self.finished = True
await show_error_popup(
await _show_error_popup(
TR.fido__title_register,
TR.fido__device_already_registered_with_template,
TR.fido__already_registered,
@ -924,7 +945,7 @@ class Fido2ConfirmNoPin(State):
await send_cmd(cmd, self.iface)
self.finished = True
await show_error_popup(
await _show_error_popup(
TR.fido__title_verify_user,
TR.fido__please_enable_pin_protection,
TR.fido__unable_to_verify_user,
@ -947,7 +968,7 @@ class Fido2ConfirmNoCredentials(Fido2ConfirmGetAssertion):
await send_cmd(cmd, self.iface)
self.finished = True
await show_error_popup(
await _show_error_popup(
TR.fido__title_authenticate,
TR.fido__not_registered_with_template,
TR.fido__not_registered,
@ -1059,6 +1080,7 @@ class DialogManager:
try:
while self.result is _RESULT_NONE:
workflow.close_others()
result = await self.state.confirm_dialog()
if isinstance(result, State):
self.state = result

View File

@ -68,7 +68,7 @@ async def bootscreen() -> None:
lockscreen = Lockscreen(
label=storage.device.get_label(), bootscreen=True
)
await lockscreen
await lockscreen.get_result()
lockscreen.__del__()
await verify_user_pin()
storage.init_unlocked()

View File

@ -7,28 +7,6 @@ if __debug__:
save_screen = False
save_screen_directory = "."
current_content_tokens: list[str] = [""] * 60
current_content_tokens.clear()
watch_layout_changes = False
layout_watcher = 0
layout_watcher = False
reset_internal_entropy: bytes = b""
class DebugEvents:
def __init__(self):
self.reset()
def reset(self) -> None:
self.last_event = 0
self.last_result: int | None = None
self.awaited_event: int | None = None
debug_events = DebugEvents()
def reset_debug_events() -> None:
debug_events.reset()
# Event resulted in the layout change, call
# notify_layout_change with this ID in first_paint of next layout.
new_layout_event_id: int | None = None

7
core/src/trezor/enums/DebugWaitType.py generated Normal file
View File

@ -0,0 +1,7 @@
# Automatically generated by pb2py
# fmt: off
# isort:skip_file
IMMEDIATE = 0
NEXT_LAYOUT = 1
CURRENT_LAYOUT = 2

View File

@ -270,6 +270,11 @@ if TYPE_CHECKING:
MIDDLE_BTN = 1
RIGHT_BTN = 2
class DebugWaitType(IntEnum):
IMMEDIATE = 0
NEXT_LAYOUT = 1
CURRENT_LAYOUT = 2
class EthereumDefinitionType(IntEnum):
NETWORK = 0
TOKEN = 1

View File

@ -671,24 +671,3 @@ class spawn(Syscall):
is True, it would be calling close on self, which will result in a ValueError.
"""
return self.task is this_task
class Timer(Syscall):
def __init__(self) -> None:
self.task: Task | None = None
# Event::Attach is evaluated before task is set. Use this list to
# buffer timers until task is set.
self.before_task: list[tuple[int, Any]] = []
def handle(self, task: Task) -> None:
self.task = task
for deadline, value in self.before_task:
schedule(self.task, value, deadline)
self.before_task.clear()
def schedule(self, deadline: int, value: Any) -> None:
deadline = utime.ticks_add(utime.ticks_ms(), deadline)
if self.task is not None:
schedule(self.task, value, deadline)
else:
self.before_task.append((deadline, value))

View File

@ -40,6 +40,7 @@ if TYPE_CHECKING:
from trezor.enums import DebugButton # noqa: F401
from trezor.enums import DebugPhysicalButton # noqa: F401
from trezor.enums import DebugSwipeDirection # noqa: F401
from trezor.enums import DebugWaitType # noqa: F401
from trezor.enums import DecredStakingSpendType # noqa: F401
from trezor.enums import EthereumDataType # noqa: F401
from trezor.enums import EthereumDefinitionType # noqa: F401
@ -2809,7 +2810,6 @@ if TYPE_CHECKING:
input: "str | None"
x: "int | None"
y: "int | None"
wait: "bool | None"
hold_ms: "int | None"
physical_button: "DebugPhysicalButton | None"
@ -2821,7 +2821,6 @@ if TYPE_CHECKING:
input: "str | None" = None,
x: "int | None" = None,
y: "int | None" = None,
wait: "bool | None" = None,
hold_ms: "int | None" = None,
physical_button: "DebugPhysicalButton | None" = None,
) -> None:
@ -2831,20 +2830,6 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkDecision"]:
return isinstance(msg, cls)
class DebugLinkLayout(protobuf.MessageType):
tokens: "list[str]"
def __init__(
self,
*,
tokens: "list[str] | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkLayout"]:
return isinstance(msg, cls)
class DebugLinkReseedRandom(protobuf.MessageType):
value: "int | None"
@ -2876,16 +2861,12 @@ if TYPE_CHECKING:
return isinstance(msg, cls)
class DebugLinkGetState(protobuf.MessageType):
wait_word_list: "bool | None"
wait_word_pos: "bool | None"
wait_layout: "bool | None"
wait_layout: "DebugWaitType"
def __init__(
self,
*,
wait_word_list: "bool | None" = None,
wait_word_pos: "bool | None" = None,
wait_layout: "bool | None" = None,
wait_layout: "DebugWaitType | None" = None,
) -> None:
pass
@ -3031,26 +3012,6 @@ if TYPE_CHECKING:
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkEraseSdCard"]:
return isinstance(msg, cls)
class DebugLinkWatchLayout(protobuf.MessageType):
watch: "bool | None"
def __init__(
self,
*,
watch: "bool | None" = None,
) -> None:
pass
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkWatchLayout"]:
return isinstance(msg, cls)
class DebugLinkResetDebugEvents(protobuf.MessageType):
@classmethod
def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkResetDebugEvents"]:
return isinstance(msg, cls)
class DebugLinkOptigaSetSecMax(protobuf.MessageType):
@classmethod

View File

@ -5,7 +5,7 @@ from . import config
if TYPE_CHECKING:
from typing import Any, Container
from trezor.ui.layouts.common import ProgressLayout
from trezor.ui import ProgressLayout
_previous_seconds: int | None = None
_previous_remaining: str | None = None

View File

@ -1,21 +1,30 @@
# pylint: disable=wrong-import-position
import utime
from micropython import const
from trezorui import Display
from typing import TYPE_CHECKING, Any, Awaitable, Generator
from typing import TYPE_CHECKING
from trezor import loop, utils
from trezorui2 import AttachType, BacklightLevels
import trezorui2
from trezor import io, log, loop, utils, wire, workflow
from trezor.messages import ButtonAck, ButtonRequest
from trezor.wire import context
from trezorui2 import BacklightLevels, LayoutState
if TYPE_CHECKING:
from typing import Generic, TypeVar
from typing import Any, Callable, Generator, Generic, Iterator, TypeVar
from trezorui2 import UiResult # noqa: F401
from trezorui2 import AttachType, LayoutObj, UiResult # noqa: F401
T = TypeVar("T")
T = TypeVar("T", covariant=True)
else:
Generic = [object]
T = 0
Generic = {T: object}
if __debug__:
trezorui2.disable_animation(bool(utils.DISABLE_ANIMATION))
# all rendering is done through a singleton of `Display`
display = Display()
@ -28,15 +37,14 @@ MONO: int = Display.FONT_MONO
WIDTH: int = Display.WIDTH
HEIGHT: int = Display.HEIGHT
# channel used to cancel layouts, see `Cancelled` exception
layout_chan = loop.chan()
_REQUEST_ANIMATION_FRAME = const(1)
"""Animation frame timer token.
See `trezor::ui::layout::base::EventCtx::ANIM_FRAME_TIMER`.
"""
# allow only one alert at a time to avoid alerts overlapping
_alert_in_progress = False
# storing last transition type, so that next layout can continue nicely
LAST_TRANSITION_OUT: AttachType | None = None
# in debug mode, display an indicator in top right corner
if __debug__:
@ -100,102 +108,406 @@ def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None:
display.backlight(val)
class Result(Exception):
class Shutdown(Exception):
pass
SHUTDOWN = Shutdown()
CURRENT_LAYOUT: "Layout | ProgressLayout | None" = None
def set_current_layout(layout: "Layout | ProgressLayout | None") -> None:
"""Set the current global layout.
All manipulation of the global `CURRENT_LAYOUT` MUST go through this function.
It ensures that the transitions are always to/from None (so that there are never
two layouts in RUNNING state), and that the debug UI is notified of the change.
"""
When components want to trigger layout completion, they do so through
raising an instance of `Result`.
global CURRENT_LAYOUT
See `Layout.__iter__` for details.
"""
# all transitions must be to/from None
assert (CURRENT_LAYOUT is None) == (layout is not None)
def __init__(self, value: Any) -> None:
super().__init__()
self.value = value
class Cancelled(Exception):
"""
Layouts can be explicitly cancelled. This usually happens when another
layout starts, because only one layout can be running at the same time,
and is done by raising `Cancelled` on the cancelled layout. Layouts
should always re-raise such exceptions.
See `Layout.__iter__` for details.
"""
CURRENT_LAYOUT = layout
class Layout(Generic[T]):
"""Python-side handler and runner for the Rust based layouts.
Wrap a `LayoutObj` instance in `Layout` to be able to display the layout, run its
event loop, and take part in global layout management. See
[docs/core/misc/layout-lifecycle.md] for details.
"""
Abstract class.
Layouts are top-level components. Only one layout can be running at the
same time. Layouts provide asynchronous interface, so a running task can
wait for the layout to complete. Layouts complete when a `Result` is
raised, usually from some of the child components.
"""
def finalize(self) -> None:
"""
Called when the layout is done. Usually overridden to allow cleanup or storing context.
"""
pass
async def __iter__(self) -> T:
"""
Run the layout and wait until it completes. Returns the result value.
Usually not overridden.
"""
if __debug__:
# we want to call notify_layout_change() when the rendering is done;
# but only the first time the layout is awaited. Here we indicate that we
# are being awaited, and in handle_rendering() we send the appropriate event
self.should_notify_layout_change = True
value = None
try:
# If any other layout is running (waiting on the layout channel),
# we close it with the Cancelled exception, and wait until it is
# closed, just to be sure.
if layout_chan.takers:
await layout_chan.put(Cancelled())
# Now, no other layout should be running. In a loop, we create new
# layout tasks and execute them in parallel, while waiting on the
# layout channel. This allows other layouts to cancel us, and the
# layout tasks to trigger restart by exiting (new tasks are created
# and we continue, because we are in a loop).
while True:
await loop.race(layout_chan.take(), *self.create_tasks())
except Result as result:
# Result exception was raised, this means this layout is complete.
value = result.value
finally:
self.finalize()
return value
if TYPE_CHECKING:
def __await__(self) -> Generator[Any, Any, T]:
return self.__iter__() # type: ignore [Coroutine[Any, Any, T@Layout]" is incompatible with "Generator[Any, Any, T@Layout]"]
else:
__await__ = __iter__
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
"""
Called from `__iter__`. Creates and returns a sequence of tasks that
run this layout. Tasks are executed in parallel. When one of them
returns, the others are closed and `create_tasks` is called again.
Usually overridden to add another tasks to the list."""
raise NotImplementedError
if __debug__:
def read_content_into(self, content_store: list[str]) -> None:
content_store.clear()
content_store.append(self.__class__.__name__)
@staticmethod
def _trace(layout: LayoutObj) -> str:
tokens = []
def callback(*args: str) -> None:
tokens.extend(args)
layout.trace(callback)
return "".join(tokens)
def __str__(self) -> str:
return f"{repr(self)}({self._trace(self.layout)[:150]})"
@staticmethod
def notify_debuglink(layout: "Layout | None") -> None:
from apps.debug import notify_layout_change
notify_layout_change(layout)
def __init__(self, layout: LayoutObj[T]) -> None:
"""Set up a layout."""
self.layout = layout
self.tasks: set[loop.Task] = set()
self.timers: dict[int, loop.Task] = {}
self.result_box = loop.mailbox()
self.button_request_box = loop.mailbox()
self.transition_out: AttachType | None = None
self.backlight_level = BacklightLevels.NORMAL
self.context: context.Context | None = None
self.state: LayoutState = LayoutState.INITIAL
def is_ready(self) -> bool:
"""True if the layout is in READY state."""
return CURRENT_LAYOUT is not self and self.result_box.is_empty()
def is_running(self) -> bool:
"""True if the layout is in RUNNING state."""
return CURRENT_LAYOUT is self
def is_stopped(self) -> bool:
"""True if the layout is in STOPPED state."""
return CURRENT_LAYOUT is not self and not self.result_box.is_empty()
def is_layout_attached(self) -> bool:
return self.state is LayoutState.ATTACHED
def start(self) -> None:
"""Start the layout, stopping any other RUNNING layout.
If the layout is already RUNNING, do nothing. If the layout is STOPPED, fail.
"""
global CURRENT_LAYOUT
# do nothing if we are already running
if self.is_running():
return
# make sure we are not restarted before picking the previous result
assert self.is_ready()
transition_in = None
# set up the global layout, shutting down any competitors
# (caller should still call `workflow.close_others()` to ensure that someone
# else will not just shut us down immediately)
if CURRENT_LAYOUT is not None:
prev_layout = CURRENT_LAYOUT
prev_layout.stop()
transition_in = prev_layout.transition_out
assert CURRENT_LAYOUT is None
# do not notify debuglink, we will do it when we receive an ATTACHED event
set_current_layout(self)
# save context
self.context = context.CURRENT_CONTEXT
# attach a timer callback and paint self
self._event(self.layout.attach_timer_fn, self._set_timer, transition_in)
# spawn all tasks
for task in self.create_tasks():
self._start_task(task)
def stop(self, _kill_taker: bool = True) -> None:
"""Stop the layout, moving out of RUNNING state and unsetting self as the
current layout.
The resulting state is either READY (if there is no result to be picked up) or
STOPPED.
When called externally, this kills any tasks that wait for the result, assuming
that the external `stop()` is a kill. When called internally, `_kill_taker` is
set to False to indicate that a result became available and that the taker
should be allowed to pick it up.
"""
global CURRENT_LAYOUT
# stop all running timers and spawned tasks
for timer in self.timers.values():
loop.close(timer)
for task in self.tasks:
if task != loop.this_task:
loop.close(task)
self.timers.clear()
self.tasks.clear()
self.transition_out = self.layout.get_transition_out()
# shut down anyone who is waiting for the result
if _kill_taker:
self.result_box.maybe_close()
if CURRENT_LAYOUT is self:
# fade to black -- backlight is off while no layout is running
backlight_fade(BacklightLevels.NONE)
set_current_layout(None)
if __debug__:
self.notify_debuglink(None)
async def get_result(self) -> T:
"""Wait for, and return, the result of this UI layout."""
if self.is_ready():
self.start()
# else we are (a) still running or (b) already stopped
try:
if self.context is not None and self.result_box.is_empty():
self._start_task(self._handle_usb_iface())
return await self.result_box
finally:
self.stop()
def request_complete_repaint(self) -> None:
"""Request a complete repaint of the layout."""
msg = self.layout.request_complete_repaint()
assert msg is None
def _event(self, event_call: Callable[..., LayoutState | None], *args: Any) -> None:
"""Process an event coming out of the Rust layout. Set is as a result and shut
down the layout if appropriate, do nothing otherwise."""
if __debug__ and CURRENT_LAYOUT is not self:
raise wire.FirmwareError("layout received an event but it is not running")
state = event_call(*args)
self.transition_out = self.layout.get_transition_out()
if state is LayoutState.DONE:
self._emit_message(self.layout.return_value())
elif state is LayoutState.ATTACHED:
self._button_request()
if __debug__:
self.notify_debuglink(self)
if state is not None:
self.state = state
if state is LayoutState.ATTACHED:
self._first_paint()
else:
self._paint()
def _button_request(self) -> None:
"""Process a button request coming out of the Rust layout."""
if __debug__ and not self.button_request_box.is_empty():
raise wire.FirmwareError(
"button request already pending -- "
"don't forget to yield your input flow from time to time ^_^"
)
res = self.layout.button_request()
if res is None:
return
if self.context is None:
return
# in production, we don't want this to fail, hence replace=True
self.button_request_box.put(res, replace=True)
def _paint(self) -> None:
"""Paint the layout and ensure that homescreen cache is properly invalidated."""
import storage.cache as storage_cache
painted = self.layout.paint()
if painted:
refresh()
if storage_cache.homescreen_shown is not None and painted:
storage_cache.homescreen_shown = None
def _first_paint(self) -> None:
"""Paint the layout for the first time after starting it.
This is a separate call in order for homescreens to be able to override and not
paint when the screen contents are still valid.
"""
# Clear the screen of any leftovers.
self.request_complete_repaint()
self._paint()
# Turn the brightness on.
backlight_fade(self.backlight_level)
def _set_timer(self, token: int, duration_ms: int) -> None:
"""Timer callback for Rust layouts."""
async def timer_task() -> None:
self.timers.pop(token)
try:
self._event(self.layout.timer, token)
except Shutdown:
pass
if token == _REQUEST_ANIMATION_FRAME and token in self.timers:
# do not schedule another animation frame if one is already scheduled
return
assert token not in self.timers
task = timer_task()
self.timers[token] = task
deadline = utime.ticks_add(utime.ticks_ms(), duration_ms)
loop.schedule(task, deadline=deadline)
def _emit_message(self, msg: Any) -> None:
"""Process a message coming out of the Rust layout. Set is as a result and shut
down the layout if appropriate, do nothing otherwise."""
# when emitting a message, there should not be another one already waiting
assert self.result_box.is_empty()
self.stop(_kill_taker=False)
self.result_box.put(msg)
raise SHUTDOWN
def create_tasks(self) -> Iterator[loop.Task]:
"""Set up background tasks for a layout.
Called from `start()`. Creates and yields a list of background tasks, typically
event handlers for different interfaces.
Override and then `yield from super().create_tasks()` to add more tasks."""
if utils.USE_BUTTON:
yield self._handle_input_iface(io.BUTTON, self.layout.button_event)
if utils.USE_TOUCH:
yield self._handle_input_iface(io.TOUCH, self.layout.touch_event)
def _handle_input_iface(
self, iface: int, event_call: Callable[..., LayoutState | None]
) -> Generator:
"""Task that is waiting for the user input."""
touch = loop.wait(iface)
try:
while True:
# Using `yield` instead of `await` to avoid allocations.
event = yield touch
workflow.idle_timer.touch()
self._event(event_call, *event)
except Shutdown:
return
finally:
touch.close()
async def _handle_usb_iface(self) -> None:
if self.context is None:
return
while True:
try:
br_code, br_name = await loop.race(
self.context.read(()),
self.button_request_box,
)
await self.context.call(
ButtonRequest(
code=br_code, pages=self.layout.page_count(), name=br_name
),
ButtonAck,
)
except Exception:
raise
def _task_finalizer(self, task: loop.Task, value: Any) -> None:
if value is None:
# all is good
if __debug__:
log.debug(__name__, "UI task exited by itself: %s", task)
return
if isinstance(value, GeneratorExit):
if __debug__:
log.debug(__name__, "UI task was stopped: %s", task)
return
if isinstance(value, BaseException):
if __debug__ and value.__class__.__name__ != "UnexpectedMessage":
log.error(
__name__, "UI task died: %s (%s)", task, value.__class__.__name__
)
try:
self._emit_message(value)
except Shutdown:
pass
if __debug__:
log.error(__name__, "UI task returned non-None: %s (%s)", task, value)
def _start_task(self, task: loop.Task) -> None:
self.tasks.add(task)
loop.schedule(task, finalizer=self._task_finalizer)
def __del__(self) -> None:
self.layout.__del__()
def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-return-type]
while not layout_chan.takers:
yield # type: ignore [awaitable-return-type]
class ProgressLayout:
"""Progress layout.
Simplified version of the general Layout object, for the purpose of showing spinners
and loaders that are shown "in the background" of a running workflow. Does not run
background tasks, does not respond to timers.
Participates in global layout management. This is to track whether the progress bar
is currently displayed, who needs to redraw and when.
"""
def __init__(self, layout: LayoutObj[UiResult]) -> None:
self.layout = layout
self.transition_out = None
def is_layout_attached(self) -> bool:
return True
def report(self, value: int, description: str | None = None) -> None:
"""Report a progress step.
Starts the layout if it is not running.
`value` can be in range from 0 to 1000.
"""
if CURRENT_LAYOUT is not self:
self.start()
if utils.DISABLE_ANIMATION:
return
msg = self.layout.progress_event(value, description or "")
assert msg is None
if self.layout.paint():
refresh()
def start(self) -> None:
global CURRENT_LAYOUT
if CURRENT_LAYOUT is not self and CURRENT_LAYOUT is not None:
CURRENT_LAYOUT.stop()
assert CURRENT_LAYOUT is None
set_current_layout(self)
self.layout.request_complete_repaint()
painted = self.layout.paint()
backlight_fade(BacklightLevels.NORMAL)
if painted:
refresh()
def stop(self) -> None:
global CURRENT_LAYOUT
if CURRENT_LAYOUT is self:
set_current_layout(None)

View File

@ -1,42 +1,98 @@
from typing import TYPE_CHECKING
from trezor import workflow
import trezorui2
from trezor import ui, utils, workflow
from trezor.enums import ButtonRequestType
from trezor.messages import ButtonAck, ButtonRequest
from trezor.wire import context
from trezor.wire import ActionCancelled, context
if TYPE_CHECKING:
from typing import Awaitable, Protocol, TypeVar
from typing import Any, Awaitable, Callable, TypeVar
T = TypeVar("T")
LayoutType = Awaitable
PropertyType = tuple[str | None, str | bytes | None]
ExceptionType = BaseException | type[BaseException]
class ProgressLayout(Protocol):
def report(self, value: int, description: str | None = None) -> None: ...
InfoFunc = Callable[[], Awaitable[None]]
T = TypeVar("T")
async def button_request(
async def _button_request(
br_name: str,
code: ButtonRequestType = ButtonRequestType.Other,
pages: int | None = None,
pages: int = 0,
) -> None:
workflow.close_others()
await context.maybe_call(
ButtonRequest(code=code, pages=pages, name=br_name), ButtonAck
ButtonRequest(code=code, pages=pages or None, name=br_name), ButtonAck
)
async def interact(
layout: LayoutType[T],
br_name: str,
layout_obj: ui.LayoutObj[T],
br_name: str | None,
br_code: ButtonRequestType = ButtonRequestType.Other,
raise_on_cancel: ExceptionType | None = ActionCancelled,
) -> T:
pages = None
if hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access attribute "page_count" for class "LayoutType"]
# We know for certain how many pages the layout will have
pages = layout.page_count() # type: ignore [Cannot access attribute "page_count" for class "LayoutType"]
await button_request(br_name, br_code, pages)
return await layout
# shut down other workflows to prevent them from interfering with the current one
workflow.close_others()
# start the layout
layout = ui.Layout(layout_obj)
layout.start()
# send the button request
if br_name is not None:
await _button_request(br_name, br_code, layout_obj.page_count())
# wait for the layout result
result = await layout.get_result()
# raise an exception if the user cancelled the action
if raise_on_cancel is not None and result is trezorui2.CANCELLED:
raise raise_on_cancel
return result
def raise_if_not_confirmed(
layout_obj: ui.LayoutObj[ui.UiResult],
br_name: str | None,
br_code: ButtonRequestType = ButtonRequestType.Other,
exc: ExceptionType = ActionCancelled,
) -> Awaitable[None]:
action = interact(layout_obj, br_name, br_code, exc)
return action # type: ignore ["UiResult" is incompatible with "None"]
async def with_info(
main_layout: ui.LayoutObj[ui.UiResult],
info_layout: ui.LayoutObj[Any],
br_name: str,
br_code: ButtonRequestType,
) -> None:
send_button_request = True
while True:
result = await interact(
main_layout, br_name if send_button_request else None, br_code
)
# raises on cancel
send_button_request = False
if result is trezorui2.CONFIRMED:
return
elif result is trezorui2.INFO:
await interact(info_layout, None, raise_on_cancel=None)
continue
else:
raise RuntimeError # unexpected result
def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None:
# Simple drawing not supported for layouts that set timers.
def dummy_set_timer(token: int, duration: int) -> None:
raise RuntimeError
layout.attach_timer_fn(dummy_set_timer, None)
if utils.USE_BACKLIGHT:
ui.backlight_fade(ui.BacklightLevels.DIM)
if layout.paint():
ui.refresh()
if utils.USE_BACKLIGHT:
ui.backlight_fade(ui.BacklightLevels.NORMAL)

View File

@ -1,8 +1,132 @@
from trezor import utils
from typing import TYPE_CHECKING
if utils.UI_LAYOUT == "TT":
from .tt.homescreen import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.homescreen import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY":
from .mercury.homescreen import * # noqa: F401,F403
import storage.cache as storage_cache
import trezorui2
from trezor import TR, ui
if TYPE_CHECKING:
from typing import Any, Iterator
from trezor import loop
class HomescreenBase(ui.Layout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _paint(self) -> None:
if 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()
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(
"!"
) # TODO handle TS5 that doesn't have it
if notification == TR.homescreen__title_coinjoin_authorized:
level = 3
elif notification == TR.homescreen__title_experimental_mode:
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,
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._event(self.layout.usb_event, is_connected)
def create_tasks(self) -> Iterator[loop.Task]:
yield from super().create_tasks()
yield self.usb_checker_task()
class Lockscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON
def __init__(
self,
label: str | None,
bootscreen: bool = False,
coinjoin_authorized: bool = False,
) -> None:
self.bootscreen = bootscreen
self.backlight_level = ui.BacklightLevels.LOW
if bootscreen:
self.backlight_level = ui.BacklightLevels.NORMAL
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label,
bootscreen=bootscreen,
skip_first_paint=skip,
coinjoin_authorized=coinjoin_authorized,
),
)
async def get_result(self) -> Any:
result = await super().get_result()
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__(
layout=trezorui2.show_progress_coinjoin(
title=TR.coinjoin__waiting_for_others,
indeterminate=True,
time_ms=delay_ms,
skip_first_paint=skip,
)
)
async def get_result(self) -> Any:
from apps.base import set_homescreen
# Handle timeout.
result = await super().get_result()
assert result == trezorui2.CANCELLED
storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
return result

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,10 @@
from typing import TYPE_CHECKING
import trezorui2
from trezor import ui
from trezor.enums import ButtonRequestType
from ..common import interact
from . import RustLayout
if TYPE_CHECKING:
from trezor.loop import AwaitableTask
if __debug__:
from trezor import io, ui
from ... import Result
# needed solely for test_emu_u2f
class _RustFidoLayoutImpl(RustLayout):
def create_tasks(self) -> tuple[AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_swipe(),
self.handle_click_signal(),
self.handle_debug_confirm(),
)
async def handle_debug_confirm(self) -> None:
from apps.debug import result_signal
_event_id, result = await result_signal()
if result is not trezorui2.CONFIRMED:
raise Result(result)
for event, x, y in (
(io.TOUCH_START, 120, 160),
(io.TOUCH_MOVE, 120, 130),
(io.TOUCH_END, 120, 100),
(io.TOUCH_START, 120, 120),
(io.TOUCH_END, 120, 120),
):
msg = self.layout.touch_event(event, x, y)
if self.layout.paint():
ui.refresh()
if msg is not None:
raise Result(msg)
_RustFidoLayout = _RustFidoLayoutImpl
else:
_RustFidoLayout = RustLayout
async def confirm_fido(
@ -58,16 +14,30 @@ async def confirm_fido(
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = _RustFidoLayout(
trezorui2.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
confirm = trezorui2.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
result = await interact(confirm, "confirm_fido", ButtonRequestType.Other)
if __debug__ and result is trezorui2.CONFIRMED:
# debuglink will directly inject a CONFIRMED message which we need to handle
# by playing back a click to the Rust layout and getting out the selected number
# that way
from trezor import io
msg = confirm.touch_event(io.TOUCH_START, 220, 220)
assert msg is None
if confirm.paint():
ui.refresh()
msg = confirm.touch_event(io.TOUCH_END, 220, 220)
if confirm.paint():
ui.refresh()
assert isinstance(msg, int)
return msg
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
@ -82,7 +52,7 @@ async def confirm_fido(
async def confirm_fido_reset() -> bool:
from trezor import TR
confirm = RustLayout(
confirm = ui.Layout(
trezorui2.confirm_action(
title=TR.fido__title_reset,
action=TR.fido__erase_credentials,
@ -91,4 +61,4 @@ async def confirm_fido_reset() -> bool:
prompt_screen=True,
)
)
return (await confirm) is trezorui2.CONFIRMED
return (await confirm.get_result()) is trezorui2.CONFIRMED

View File

@ -1,149 +0,0 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
import trezorui2
from trezor import TR, ui
from . import RustLayout
if TYPE_CHECKING:
from typing import Any, Tuple
from trezor import loop
class HomescreenBase(RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _attach(self) -> None:
if storage_cache.homescreen_shown is self.RENDER_INDICATOR:
self.layout.attach_timer_fn(self.set_timer, ui.AttachType.RESUME)
else:
self.layout.attach_timer_fn(self.set_timer, ui.LAST_TRANSITION_OUT)
def _paint(self) -> None:
if 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()
if __debug__:
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_click_signal(), # so we can receive debug events
)
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:
if notification == TR.homescreen__title_coinjoin_authorized:
level = 3
elif notification == TR.homescreen__title_experimental_mode:
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,
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)
if 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
def __init__(
self,
label: str | None,
bootscreen: bool = False,
coinjoin_authorized: bool = False,
) -> None:
self.bootscreen = bootscreen
self.backlight_level = ui.BacklightLevels.LOW
if bootscreen:
self.backlight_level = ui.BacklightLevels.NORMAL
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label,
bootscreen=bootscreen,
skip_first_paint=skip,
coinjoin_authorized=coinjoin_authorized,
),
)
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:
from trezor import TR
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_progress_coinjoin(
title=TR.coinjoin__waiting_for_others,
indeterminate=True,
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

View File

@ -1,11 +1,11 @@
from typing import TYPE_CHECKING
import trezorui2
from trezor import TR
from trezor import TR, ui
from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact
from . import RustLayout, raise_if_not_confirmed
from . import raise_if_not_confirmed
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
CANCELLED = trezorui2.CANCELLED # global_import_cache
@ -16,30 +16,36 @@ if TYPE_CHECKING:
async def request_word_count(recovery_type: RecoveryType) -> int:
selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type))
count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount)
selector = trezorui2.select_word_count(recovery_type=recovery_type)
count = await interact(
selector, "recovery_word_count", ButtonRequestType.MnemonicWordCount
)
return int(count)
async def request_word(
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0
if is_slip39:
keyboard = RustLayout(
trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
keyboard = trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
keyboard = RustLayout(
trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
keyboard = trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await keyboard
word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word
@ -78,20 +84,16 @@ def format_remaining_shares_info(
async def show_group_share_success(share_index: int, group_index: int) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
)
),
"share_success",
ButtonRequestType.Other,
)
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
),
"share_success",
ButtonRequestType.Other,
)
@ -103,9 +105,7 @@ async def continue_recovery(
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
# NOTE: show_info can be understood as first screen before any shares
# NOTE: button request sent from the flow
result = await RustLayout(
result = await interact(
trezorui2.flow_continue_recovery(
first_screen=show_info,
recovery_type=recovery_type,
@ -116,7 +116,10 @@ async def continue_recovery(
if remaining_shares_info
else None
),
)
),
None,
ButtonRequestType.Other,
raise_on_cancel=None,
)
return result is CONFIRMED
@ -130,17 +133,13 @@ async def show_recovery_warning(
) -> None:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=content or TR.words__warning,
value=subheader or "",
button=button,
description="",
danger=True,
)
),
br_name,
br_code,
)
trezorui2.show_warning(
title=content or TR.words__warning,
value=subheader or "",
button=button,
description="",
danger=True,
),
br_name,
br_code,
)

View File

@ -1,27 +1,21 @@
from typing import TYPE_CHECKING
from typing import Awaitable, Callable, Sequence
import trezorui2
from trezor import TR
from trezor import TR, ui
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import RustLayout, raise_if_not_confirmed, show_success
if TYPE_CHECKING:
pass
from typing import Callable, Sequence
from . import raise_if_not_confirmed, show_success
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
async def show_share_words(
def show_share_words(
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
) -> Awaitable[None]:
title = TR.reset__recovery_wallet_backup_title
if share_index is None:
subtitle = ""
@ -43,7 +37,7 @@ async def show_share_words(
text_info.append(TR.reset__repeat_for_all_shares)
text_confirm = TR.reset__words_written_down_template.format(words_count)
result = await RustLayout(
return raise_if_not_confirmed(
trezorui2.flow_show_share_words(
title=title,
subtitle=subtitle,
@ -51,12 +45,10 @@ async def show_share_words(
description=description,
text_info=text_info,
text_confirm=text_confirm,
)
),
None,
)
if result != CONFIRMED:
raise ActionCancelled
async def select_word(
words: Sequence[str],
@ -81,14 +73,15 @@ async def select_word(
while len(words) < 3:
words.append(words[-1])
result = await RustLayout(
result = await interact(
trezorui2.select_word(
title=title,
description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count
),
words=(words[0], words[1], words[2]),
)
),
None,
)
if __debug__ and isinstance(result, str):
return result
@ -104,13 +97,11 @@ async def slip39_show_checklist(
) -> None:
items = _slip_39_checklist_items(step, advanced, count, threshold)
result = await interact(
RustLayout(
trezorui2.show_checklist(
title=TR.reset__title_shamir_backup,
button=TR.buttons__continue,
active=step,
items=items,
)
trezorui2.show_checklist(
title=TR.reset__title_shamir_backup,
button=TR.buttons__continue,
active=step,
items=items,
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
@ -162,8 +153,7 @@ async def _prompt_number(
max_count: int,
br_name: str,
) -> int:
result = await RustLayout(
result = await interact(
trezorui2.flow_request_number(
title=title,
description=description,
@ -173,7 +163,8 @@ async def _prompt_number(
info=info,
br_code=ButtonRequestType.ResetDevice,
br_name=br_name,
)
),
None,
)
if __debug__ and result is CONFIRMED:
@ -188,9 +179,9 @@ async def _prompt_number(
raise ActionCancelled # user cancelled request number prompt
async def slip39_prompt_threshold(
def slip39_prompt_threshold(
num_of_shares: int, group_id: int | None = None
) -> int:
) -> Awaitable[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 advanced slip39
@ -212,7 +203,7 @@ async def slip39_prompt_threshold(
)
)
return await _prompt_number(
return _prompt_number(
TR.reset__title_set_threshold,
description,
info,
@ -298,63 +289,53 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non
description = TR.backup__info_multi_share_backup
await interact(
RustLayout(
trezorui2.show_info(
title=TR.backup__title_create_wallet_backup, description=description
)
trezorui2.show_info(
title=TR.backup__title_create_wallet_backup, description=description
),
"backup_intro",
ButtonRequestType.ResetDevice,
)
def show_warning_backup() -> Awaitable[ui.UiResult]:
return interact(
trezorui2.show_warning(
title=TR.words__important,
value=TR.reset__never_make_digital_copy,
button="",
allow_cancel=False,
danger=False, # Use a less severe icon color
),
"backup_warning",
ButtonRequestType.ResetDevice,
)
async def show_warning_backup() -> None:
result = await interact(
RustLayout(
trezorui2.show_warning(
title=TR.words__important,
value=TR.reset__never_make_digital_copy,
button="",
allow_cancel=False,
danger=False, # Use a less severe icon color
)
),
"backup_warning",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def show_success_backup() -> None:
await show_success(
def show_success_backup() -> Awaitable[None]:
return show_success(
"success_backup",
TR.backup__title_backup_completed,
)
async def show_reset_warning(
def show_reset_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=subheader or "",
description=content,
value="",
button="",
allow_cancel=False,
danger=True,
)
),
br_name,
br_code,
)
) -> Awaitable[None]:
return raise_if_not_confirmed(
trezorui2.show_warning(
title=subheader or "",
description=content,
value="",
button="",
allow_cancel=False,
danger=True,
),
br_name,
br_code,
)
@ -391,11 +372,11 @@ async def show_share_confirmation_success(
)
)
return await show_success("success_recovery", title, subheader=footer_description)
await show_success("success_recovery", title, subheader=footer_description)
async def show_share_confirmation_failure() -> None:
await show_reset_warning(
def show_share_confirmation_failure() -> Awaitable[None]:
return show_reset_warning(
"warning_backup_check",
TR.words__try_again,
TR.reset__incorrect_word_selected,

View File

@ -1,13 +1,6 @@
from typing import TYPE_CHECKING
import trezorui2
from trezor import TR, config, ui, utils
if TYPE_CHECKING:
from typing import Any
from .common import ProgressLayout
def _storage_message_to_str(message: config.StorageMessage | None) -> str | None:
from trezor import TR
@ -28,37 +21,15 @@ def _storage_message_to_str(message: config.StorageMessage | None) -> str | None
raise RuntimeError # unknown message
class RustProgress:
def __init__(
self,
layout: Any,
):
self.layout = layout
ui.backlight_fade(ui.BacklightLevels.DIM)
self.layout.attach_timer_fn(self.set_timer, None)
if self.layout.paint():
ui.refresh()
ui.backlight_fade(ui.BacklightLevels.NORMAL)
def set_timer(self, token: int, duration_ms: 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
if self.layout.paint():
ui.refresh()
def progress(
description: str | None = None,
title: str | None = None,
indeterminate: bool = False,
) -> ProgressLayout:
) -> ui.ProgressLayout:
if description is None:
description = TR.progress__please_wait # def_arg
return RustProgress(
return ui.ProgressLayout(
layout=trezorui2.show_progress(
description=description,
title=title,
@ -67,27 +38,27 @@ def progress(
)
def bitcoin_progress(message: str) -> ProgressLayout:
def bitcoin_progress(message: str) -> ui.ProgressLayout:
return progress(message)
def coinjoin_progress(message: str) -> ProgressLayout:
return RustProgress(
def coinjoin_progress(message: str) -> ui.ProgressLayout:
return ui.ProgressLayout(
layout=trezorui2.show_progress_coinjoin(title=message, indeterminate=False)
)
def pin_progress(title: config.StorageMessage, description: str) -> ProgressLayout:
def pin_progress(title: config.StorageMessage, description: str) -> ui.ProgressLayout:
return progress(description=description, title=_storage_message_to_str(title))
if not utils.BITCOIN_ONLY:
def monero_keyimage_sync_progress() -> ProgressLayout:
def monero_keyimage_sync_progress() -> ui.ProgressLayout:
return progress(TR.progress__syncing)
def monero_live_refresh_progress() -> ProgressLayout:
def monero_live_refresh_progress() -> ui.ProgressLayout:
return progress(TR.progress__refreshing, indeterminate=True)
def monero_transaction_progress_inner() -> ProgressLayout:
def monero_transaction_progress_inner() -> ui.ProgressLayout:
return progress(TR.progress__signing_transaction)

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
import trezorui2
from trezor import ui
from trezor.enums import ButtonRequestType
from ..common import interact
from . import RustLayout
async def confirm_fido(
@ -12,17 +12,13 @@ async def confirm_fido(
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = RustLayout(
trezorui2.confirm_fido( # type: ignore [Argument missing for parameter "icon_name"]
title=header,
app_name=app_name,
accounts=accounts,
)
confirm = trezorui2.confirm_fido( # type: ignore [Argument missing for parameter "icon_name"]
title=header,
app_name=app_name,
accounts=accounts,
)
result = await interact(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
@ -31,22 +27,17 @@ async def confirm_fido(
if __debug__ and result is trezorui2.CONFIRMED:
return 0
# Late import won't get executed on the happy path.
from trezor.wire import ActionCancelled
raise ActionCancelled
raise RuntimeError # should not get here, cancellation is handled by `interact`
async def confirm_fido_reset() -> bool:
from trezor import TR
confirm = RustLayout(
trezorui2.confirm_action(
title=TR.fido__title_reset,
description=TR.fido__wanna_erase_credentials,
action=None,
verb_cancel="",
verb=TR.buttons__confirm,
)
confirm = trezorui2.confirm_action(
title=TR.fido__title_reset,
description=TR.fido__wanna_erase_credentials,
action=None,
verb_cancel="",
verb=TR.buttons__confirm,
)
return (await confirm) is trezorui2.CONFIRMED
return (await ui.Layout(confirm).get_result()) is trezorui2.CONFIRMED

View File

@ -1,128 +0,0 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
import trezorui2
from trezor import TR, ui
from . import RustLayout
if TYPE_CHECKING:
from typing import Any, Tuple
from trezor import loop
class HomescreenBase(RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _paint(self) -> None:
if 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()
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:
if notification == TR.homescreen__title_experimental_mode:
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,
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)
if 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
def __init__(
self,
label: str | None,
bootscreen: bool = False,
coinjoin_authorized: bool = False,
) -> None:
self.bootscreen = bootscreen
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label,
bootscreen=bootscreen,
skip_first_paint=skip,
coinjoin_authorized=coinjoin_authorized,
),
)
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:
from trezor import TR
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_progress_coinjoin(
title=TR.coinjoin__waiting_for_others,
indeterminate=True,
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

View File

@ -1,20 +1,22 @@
from typing import TYPE_CHECKING, Iterable
from typing import TYPE_CHECKING
import trezorui2
from trezor import TR
from trezor import TR, ui
from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact
from . import RustLayout, raise_if_not_confirmed, show_warning
from . import show_warning
if TYPE_CHECKING:
from typing import Awaitable, Iterable
from apps.management.recovery_device.layout import RemainingSharesInfo
async def request_word_count(recovery_type: RecoveryType) -> int:
count = await interact(
RustLayout(trezorui2.select_word_count(recovery_type=recovery_type)),
"word_count",
trezorui2.select_word_count(recovery_type=recovery_type),
"recovery_word_count",
ButtonRequestType.MnemonicWordCount,
)
# It can be returning a string (for example for __debug__ in tests)
@ -22,26 +24,31 @@ async def request_word_count(recovery_type: RecoveryType) -> int:
async def request_word(
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0
if is_slip39:
word_choice = RustLayout(
trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
)
else:
word_choice = RustLayout(
trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
keyboard = trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await word_choice
else:
keyboard = trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word
@ -53,22 +60,20 @@ async def show_remaining_shares(
raise NotImplementedError
async def show_group_share_success(share_index: int, group_index: int) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
)
),
"share_success",
ButtonRequestType.Other,
)
def show_group_share_success(
share_index: int, group_index: int
) -> Awaitable[ui.UiResult]:
return interact(
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
),
"share_success",
ButtonRequestType.Other,
)
@ -117,26 +122,24 @@ async def continue_recovery(
if subtext:
text += f"\n\n{subtext}"
homepage = trezorui2.confirm_recovery(
title="",
description=text,
button=button_label,
recovery_type=recovery_type,
info_button=False,
show_info=show_info, # type: ignore [No parameter named "show_info"]
)
while True:
homepage = RustLayout(
trezorui2.confirm_recovery(
title="",
description=text,
button=button_label,
recovery_type=recovery_type,
info_button=False,
show_info=show_info, # type: ignore [No parameter named "show_info"]
)
)
result = await interact(
homepage,
"recovery",
ButtonRequestType.RecoveryHomepage,
raise_on_cancel=None,
)
if result is trezorui2.CONFIRMED:
return True
# user has chosen to abort, confirm the choice
try:
await _confirm_abort(recovery_type != RecoveryType.NormalRecovery)
except ActionCancelled:
@ -145,12 +148,12 @@ async def continue_recovery(
return False
async def show_recovery_warning(
def show_recovery_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
) -> Awaitable[ui.UiResult]:
button = button or TR.buttons__try_again # def_arg
await show_warning(br_name, content, subheader, button, br_code=br_code)
return show_warning(br_name, content, subheader, button, br_code=br_code)

View File

@ -1,12 +1,11 @@
from typing import Sequence
from typing import Awaitable, Sequence
import trezorui2
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import RustLayout, confirm_action, show_success, show_warning
from ..common import interact, raise_if_not_confirmed
from . import confirm_action, show_success, show_warning
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
@ -45,13 +44,12 @@ async def show_share_words(
)
result = await interact(
RustLayout(
trezorui2.show_share_words( # type: ignore [Arguments missing for parameters]
share_words=share_words, # type: ignore [No parameter named "share_words"]
)
trezorui2.show_share_words( # type: ignore [Arguments missing for parameters]
share_words=share_words, # type: ignore [No parameter named "share_words"]
),
br_name,
br_code,
raise_on_cancel=None,
)
if result is CONFIRMED:
break
@ -83,12 +81,13 @@ async def select_word(
words.append(words[-1])
word_ordinal = format_ordinal(checked_index + 1)
result = await RustLayout(
result = await interact(
trezorui2.select_word(
title="",
description=TR.reset__select_word_template.format(word_ordinal),
words=(words[0].lower(), words[1].lower(), words[2].lower()),
)
),
None,
)
if __debug__ and isinstance(result, str):
return result
@ -96,12 +95,12 @@ async def select_word(
return words[result]
async def slip39_show_checklist(
def slip39_show_checklist(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> None:
) -> Awaitable[None]:
items = (
(
TR.reset__slip39_checklist_num_shares,
@ -116,20 +115,16 @@ async def slip39_show_checklist(
)
)
result = await interact(
RustLayout(
trezorui2.show_checklist(
title=TR.reset__slip39_checklist_title,
button=TR.buttons__continue,
active=step,
items=items,
)
return raise_if_not_confirmed(
trezorui2.show_checklist(
title=TR.reset__slip39_checklist_title,
button=TR.buttons__continue,
active=step,
items=items,
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result is not CONFIRMED:
raise ActionCancelled
async def _prompt_number(
@ -139,13 +134,11 @@ async def _prompt_number(
max_count: int,
br_name: str,
) -> int:
num_input = RustLayout(
trezorui2.request_number(
title=title,
count=count,
min_count=min_count,
max_count=max_count,
)
num_input = trezorui2.request_number(
title=title,
count=count,
min_count=min_count,
max_count=max_count,
)
result = await interact(
@ -225,12 +218,12 @@ async def slip39_prompt_number_of_shares(
)
async def slip39_advanced_prompt_number_of_groups() -> int:
def slip39_advanced_prompt_number_of_groups() -> Awaitable[int]:
count = 5
min_count = 2
max_count = 16
return await _prompt_number(
return _prompt_number(
TR.reset__title_number_of_groups,
count,
min_count,
@ -239,12 +232,12 @@ async def slip39_advanced_prompt_number_of_groups() -> int:
)
async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> Awaitable[int]:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
return await _prompt_number(
return _prompt_number(
TR.reset__title_group_threshold,
count,
min_count,
@ -253,15 +246,15 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
)
async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None:
def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable[None]:
if single_share:
assert num_of_words is not None
description = TR.backup__info_single_share_backup.format(num_of_words)
else:
description = TR.backup__info_multi_share_backup
await confirm_action(
"backup_warning",
return confirm_action(
"backup_intro",
title=TR.backup__title_backup_wallet,
verb=TR.buttons__continue,
description=description,
@ -270,8 +263,8 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non
)
async def show_warning_backup() -> None:
await show_warning(
def show_warning_backup() -> Awaitable[trezorui2.UiResult]:
return show_warning(
"backup_warning",
TR.words__title_remember,
TR.reset__never_make_digital_copy,
@ -280,8 +273,8 @@ async def show_warning_backup() -> None:
)
async def show_success_backup() -> None:
await confirm_action(
def show_success_backup() -> Awaitable[None]:
return confirm_action(
"success_backup",
TR.reset__title_backup_is_done,
description=TR.words__keep_it_safe,
@ -291,16 +284,16 @@ async def show_success_backup() -> None:
)
async def show_reset_warning(
def show_reset_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
) -> Awaitable[trezorui2.UiResult]:
button = button or TR.buttons__try_again # def_arg
await show_warning(
return show_warning(
br_name,
subheader or "",
content,

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,8 @@
from typing import TYPE_CHECKING
import trezorui2
from trezor import ui
from trezor.enums import ButtonRequestType
from ..common import interact
from . import RustLayout
if TYPE_CHECKING:
from trezor.loop import AwaitableTask
if __debug__:
from trezor import io, ui
from ... import Result
class _RustFidoLayoutImpl(RustLayout):
def create_tasks(self) -> tuple[AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_swipe(),
self.handle_debug_confirm(),
)
async def handle_debug_confirm(self) -> None:
from apps.debug import result_signal
_event_id, result = await result_signal()
if result is not trezorui2.CONFIRMED:
raise Result(result)
for event, x, y in (
(io.TOUCH_START, 220, 220),
(io.TOUCH_END, 220, 220),
):
msg = self.layout.touch_event(event, x, y)
if self.layout.paint():
ui.refresh()
if msg is not None:
raise Result(msg)
_RustFidoLayout = _RustFidoLayoutImpl
else:
_RustFidoLayout = RustLayout
async def confirm_fido(
@ -54,16 +12,31 @@ async def confirm_fido(
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = _RustFidoLayout(
trezorui2.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
confirm = trezorui2.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
result = await interact(confirm, "confirm_fido", ButtonRequestType.Other)
if __debug__ and result is trezorui2.CONFIRMED:
# debuglink will directly inject a CONFIRMED message which we need to handle
# by playing back a click to the Rust layout and getting out the selected number
# that way
from trezor import io
confirm.touch_event(io.TOUCH_START, 220, 220)
if confirm.paint():
ui.refresh()
msg = confirm.touch_event(io.TOUCH_END, 220, 220)
if confirm.paint():
ui.refresh()
assert msg is trezorui2.LayoutState.DONE
retval = confirm.return_value()
assert isinstance(retval, int)
return retval
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
@ -78,7 +51,7 @@ async def confirm_fido(
async def confirm_fido_reset() -> bool:
from trezor import TR
confirm = RustLayout(
confirm = ui.Layout(
trezorui2.confirm_action(
title=TR.fido__title_reset,
action=TR.fido__erase_credentials,
@ -86,4 +59,4 @@ async def confirm_fido_reset() -> bool:
reverse=True,
)
)
return (await confirm) is trezorui2.CONFIRMED
return (await confirm.get_result()) is trezorui2.CONFIRMED

View File

@ -1,143 +0,0 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
import trezorui2
from trezor import TR, ui
from . import RustLayout
if TYPE_CHECKING:
from typing import Any, Tuple
from trezor import loop
class HomescreenBase(RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
def _paint(self) -> None:
if 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()
if __debug__:
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_click_signal(), # so we can receive debug events
)
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:
if notification == TR.homescreen__title_coinjoin_authorized:
level = 3
elif notification == TR.homescreen__title_experimental_mode:
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,
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)
if 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
def __init__(
self,
label: str | None,
bootscreen: bool = False,
coinjoin_authorized: bool = False,
) -> None:
self.bootscreen = bootscreen
self.backlight_level = ui.BacklightLevels.LOW
if bootscreen:
self.backlight_level = ui.BacklightLevels.NORMAL
skip = (
not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR
)
super().__init__(
layout=trezorui2.show_lockscreen(
label=label,
bootscreen=bootscreen,
skip_first_paint=skip,
coinjoin_authorized=coinjoin_authorized,
),
)
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:
from trezor import TR
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__(
layout=trezorui2.show_progress_coinjoin(
title=TR.coinjoin__waiting_for_others,
indeterminate=True,
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

View File

@ -1,66 +1,60 @@
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING
import trezorui2
from trezor import TR
from trezor.enums import ButtonRequestType, RecoveryType
from trezor import TR, ui
from trezor.enums import ButtonRequestType
from ..common import interact
from . import RustLayout, raise_if_not_confirmed
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
INFO = trezorui2.INFO # global_import_cache
if TYPE_CHECKING:
from typing import Awaitable
from trezor.enums import RecoveryType
from apps.management.recovery_device.layout import RemainingSharesInfo
async def _homepage_with_info(
dialog: RustLayout,
info_func: Callable,
) -> trezorui2.UiResult:
while True:
result = await dialog
if result is INFO:
await info_func()
dialog.request_complete_repaint()
else:
return result
async def request_word_count(recovery_type: RecoveryType) -> int:
selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type))
count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount)
count = await interact(
trezorui2.select_word_count(recovery_type=recovery_type),
"word_count",
ButtonRequestType.MnemonicWordCount,
)
return int(count)
async def request_word(
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str:
prompt = TR.recovery__type_word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0
if is_slip39:
keyboard = RustLayout(
trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
)
else:
keyboard = RustLayout(
trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
keyboard = trezorui2.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await keyboard
else:
keyboard = trezorui2.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word
async def show_remaining_shares(
def show_remaining_shares(
groups: set[tuple[str, ...]],
shares_remaining: list[int],
group_threshold: int,
) -> None:
) -> Awaitable[trezorui2.UiResult]:
from trezor import strings
from trezor.crypto.slip39 import MAX_SHARE_COUNT
@ -86,31 +80,27 @@ async def show_remaining_shares(
words = "\n".join(group)
pages.append((title, words))
await raise_if_not_confirmed(
interact(
RustLayout(trezorui2.show_remaining_shares(pages=pages)),
"show_shares",
ButtonRequestType.Other,
)
return interact(
trezorui2.show_remaining_shares(pages=pages),
"show_shares",
ButtonRequestType.Other,
)
async def show_group_share_success(share_index: int, group_index: int) -> None:
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
)
),
"share_success",
ButtonRequestType.Other,
)
def show_group_share_success(
share_index: int, group_index: int
) -> Awaitable[ui.UiResult]:
return interact(
trezorui2.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
),
"share_success",
ButtonRequestType.Other,
)
@ -146,70 +136,60 @@ async def continue_recovery(
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
from trezor.enums import RecoveryType
from trezor.wire import ActionCancelled
from ..common import button_request
if show_info:
if show_instructions:
# Show this just one-time
description = TR.recovery__enter_each_word
else:
description = subtext or ""
homepage = trezorui2.confirm_recovery(
title=text,
description=description,
button=button_label,
recovery_type=recovery_type,
info_button=remaining_shares_info is not None,
)
while True:
homepage = RustLayout(
trezorui2.confirm_recovery(
title=text,
description=description,
button=button_label,
recovery_type=recovery_type,
info_button=remaining_shares_info is not None,
)
result = await interact(
homepage,
"recovery",
ButtonRequestType.RecoveryHomepage,
raise_on_cancel=None,
)
await button_request("recovery", ButtonRequestType.RecoveryHomepage)
if remaining_shares_info is None:
result = await homepage
else:
groups, shares_remaining, group_threshold = remaining_shares_info
result = await _homepage_with_info(
homepage,
lambda: show_remaining_shares(
groups, shares_remaining, group_threshold
),
)
if result is CONFIRMED:
if result is trezorui2.CONFIRMED:
return True
try:
await _confirm_abort(recovery_type != RecoveryType.NormalRecovery)
except ActionCancelled:
pass
elif result is trezorui2.INFO and remaining_shares_info is not None:
await show_remaining_shares(*remaining_shares_info)
else:
return False
try:
await _confirm_abort(recovery_type != RecoveryType.NormalRecovery)
except ActionCancelled:
pass
else:
return False
async def show_recovery_warning(
def show_recovery_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
) -> Awaitable[ui.UiResult]:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=content,
description=subheader or "",
button=button,
allow_cancel=False,
)
),
br_name,
br_code,
)
return interact(
trezorui2.show_warning(
title=content,
description=subheader or "",
button=button,
allow_cancel=False,
),
br_name,
br_code,
)

View File

@ -1,12 +1,10 @@
from typing import Callable, Sequence
from typing import Awaitable, Callable, Sequence
import trezorui2
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import RustLayout, raise_if_not_confirmed, show_success
from ..common import interact, raise_if_not_confirmed
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
@ -35,11 +33,11 @@ def _split_share_into_pages(share_words: Sequence[str], per_page: int = 4) -> li
return pages
async def show_share_words(
def show_share_words(
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> None:
) -> Awaitable[None]:
if share_index is None:
title = TR.reset__recovery_wallet_backup_title
elif group_index is None:
@ -51,18 +49,14 @@ async def show_share_words(
pages = _split_share_into_pages(share_words)
result = await interact(
RustLayout(
trezorui2.show_share_words(
title=title,
pages=pages,
),
return raise_if_not_confirmed(
trezorui2.show_share_words(
title=title,
pages=pages,
),
"backup_words",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def select_word(
@ -88,14 +82,15 @@ async def select_word(
while len(words) < 3:
words.append(words[-1])
result = await RustLayout(
result = await interact(
trezorui2.select_word(
title=title,
description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count
),
words=(words[0], words[1], words[2]),
)
),
None,
)
if __debug__ and isinstance(result, str):
return result
@ -103,12 +98,12 @@ async def select_word(
return words[result]
async def slip39_show_checklist(
def slip39_show_checklist(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> None:
) -> Awaitable[None]:
items = (
(
TR.reset__slip39_checklist_set_num_shares,
@ -123,20 +118,16 @@ async def slip39_show_checklist(
)
)
result = await interact(
RustLayout(
trezorui2.show_checklist(
title=TR.reset__slip39_checklist_title,
button=TR.buttons__continue,
active=step,
items=items,
)
return raise_if_not_confirmed(
trezorui2.show_checklist(
title=TR.reset__slip39_checklist_title,
button=TR.buttons__continue,
active=step,
items=items,
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def _prompt_number(
@ -148,14 +139,12 @@ async def _prompt_number(
max_count: int,
br_name: str,
) -> int:
num_input = RustLayout(
trezorui2.request_number(
title=title,
description=description,
count=count,
min_count=min_count,
max_count=max_count,
)
num_input = trezorui2.request_number(
title=title,
description=description,
count=count,
min_count=min_count,
max_count=max_count,
)
while True:
@ -163,31 +152,33 @@ async def _prompt_number(
num_input,
br_name,
ButtonRequestType.ResetDevice,
raise_on_cancel=None,
)
if __debug__:
if not isinstance(result, tuple):
# DebugLink currently can't send number of shares and it doesn't
# change the counter either so just use the initial value.
result = (result, count)
result = result, count
status, value = result
if status == CONFIRMED:
assert isinstance(value, int)
return value
await RustLayout(
await interact(
trezorui2.show_simple(
title=None,
description=info(value),
button=TR.buttons__ok_i_understand,
)
),
None,
raise_on_cancel=None,
)
num_input.request_complete_repaint()
async def slip39_prompt_threshold(
def slip39_prompt_threshold(
num_of_shares: int, group_id: int | None = None
) -> int:
) -> Awaitable[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 advanced slip39
@ -230,7 +221,7 @@ async def slip39_prompt_threshold(
text += " " + TR.reset__to_form_group_template.format(group_id + 1)
return text
return await _prompt_number(
return _prompt_number(
TR.reset__title_set_threshold,
description,
info,
@ -241,9 +232,9 @@ async def slip39_prompt_threshold(
)
async def slip39_prompt_number_of_shares(
def slip39_prompt_number_of_shares(
num_words: int, group_id: int | None = None
) -> int:
) -> Awaitable[int]:
count = 5
min_count = 1
max_count = 16
@ -266,7 +257,7 @@ async def slip39_prompt_number_of_shares(
num_words, group_id + 1
)
return await _prompt_number(
return _prompt_number(
TR.reset__title_set_number_of_shares,
description,
lambda i: info,
@ -277,14 +268,14 @@ async def slip39_prompt_number_of_shares(
)
async def slip39_advanced_prompt_number_of_groups() -> int:
def slip39_advanced_prompt_number_of_groups() -> Awaitable[int]:
count = 5
min_count = 2
max_count = 16
description = TR.reset__group_description
info = TR.reset__group_info
return await _prompt_number(
return _prompt_number(
TR.reset__title_set_number_of_groups,
lambda i: description,
lambda i: info,
@ -295,14 +286,14 @@ async def slip39_advanced_prompt_number_of_groups() -> int:
)
async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> Awaitable[int]:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
description = TR.reset__required_number_of_groups
info = TR.reset__advanced_group_threshold_info
return await _prompt_number(
return _prompt_number(
TR.reset__title_set_group_threshold,
lambda i: description,
lambda i: info,
@ -313,44 +304,40 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
)
async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None:
def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable[None]:
if single_share:
assert num_of_words is not None
description = TR.backup__info_single_share_backup.format(num_of_words)
else:
description = TR.backup__info_multi_share_backup
await interact(
RustLayout(
trezorui2.show_info(
title="",
button=TR.buttons__continue,
description=description,
allow_cancel=False,
)
return raise_if_not_confirmed(
trezorui2.show_info(
title="",
button=TR.buttons__continue,
description=description,
allow_cancel=False,
),
"backup_warning",
"backup_intro",
ButtonRequestType.ResetDevice,
)
async def show_warning_backup() -> None:
result = await interact(
RustLayout(
trezorui2.show_info(
title=TR.reset__never_make_digital_copy,
button=TR.buttons__ok_i_understand,
allow_cancel=False,
)
def show_warning_backup() -> Awaitable[trezorui2.UiResult]:
return interact(
trezorui2.show_info(
title=TR.reset__never_make_digital_copy,
button=TR.buttons__ok_i_understand,
allow_cancel=False,
),
"backup_warning",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
async def show_success_backup() -> None:
from . import show_success
await show_success(
"success_backup",
TR.reset__use_your_backup,
@ -358,27 +345,23 @@ async def show_success_backup() -> None:
)
async def show_reset_warning(
def show_reset_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
) -> Awaitable[trezorui2.UiResult]:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.show_warning(
title=subheader or "",
description=content,
button=button,
allow_cancel=False,
)
),
br_name,
br_code,
)
return interact(
trezorui2.show_warning(
title=subheader or "",
description=content,
button=button,
allow_cancel=False,
),
br_name,
br_code,
)
@ -387,6 +370,8 @@ async def show_share_confirmation_success(
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
from . import show_success
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
subheader = TR.reset__finished_verifying_wallet_backup

View File

@ -172,21 +172,6 @@ class Context:
CURRENT_CONTEXT: Context | None = None
def wait(task: Awaitable[T]) -> Awaitable[T]:
"""
Wait until the passed in task finishes, and return the result, while servicing the
wire context.
Used to make sure the device is responsive on USB while waiting for user
interaction. If a message is received before the task finishes, it raises an
`UnexpectedMessage` exception, returning control to the session handler.
"""
if CURRENT_CONTEXT is None:
return task
else:
return loop.race(CURRENT_CONTEXT.read(()), task)
async def call(
msg: protobuf.MessageType,
expected_type: type[LoadedMessageType],

View File

@ -19,6 +19,7 @@ import logging
import re
import textwrap
import time
from contextlib import contextmanager
from copy import deepcopy
from datetime import datetime
from enum import Enum, IntEnum, auto
@ -38,19 +39,20 @@ from typing import (
Tuple,
Type,
Union,
overload,
)
from mnemonic import Mnemonic
from typing_extensions import Literal
from . import mapping, messages, models, protobuf
from .client import TrezorClient
from .exceptions import TrezorFailure
from .log import DUMP_BYTES
from .messages import DebugWaitType
from .tools import expect
if TYPE_CHECKING:
from typing_extensions import Protocol
from .messages import PinMatrixRequestType
from .transport import Transport
@ -60,6 +62,15 @@ if TYPE_CHECKING:
AnyDict = Dict[str, Any]
class InputFunc(Protocol):
def __call__(
self,
hold_ms: Optional[int] = None,
wait: Optional[bool] = None,
) -> "LayoutContent":
...
EXPECTED_RESPONSES_CONTEXT_LINES = 3
LOG = logging.getLogger(__name__)
@ -391,6 +402,29 @@ def multipage_content(layouts: List[LayoutContent]) -> str:
return "".join(layout.text_content() for layout in layouts)
def _make_input_func(
button: Optional[messages.DebugButton] = None,
physical_button: Optional[messages.DebugPhysicalButton] = None,
swipe: Optional[messages.DebugSwipeDirection] = None,
) -> "InputFunc":
decision = messages.DebugLinkDecision(
button=button,
physical_button=physical_button,
swipe=swipe,
)
def input_func(
self: "DebugLink",
hold_ms: Optional[int] = None,
wait: Optional[bool] = None,
) -> LayoutContent:
__tracebackhide__ = True # for pytest # pylint: disable=W0612
decision.hold_ms = hold_ms
return self._decision(decision, wait=wait)
return input_func # type: ignore [Parameter name mismatch]
class DebugLink:
def __init__(self, transport: "Transport", auto_interact: bool = True) -> None:
self.transport = transport
@ -405,7 +439,6 @@ class DebugLink:
self.screenshot_recording_dir: Optional[str] = None
# For T1 screenshotting functionality in DebugUI
self.t1_take_screenshots = False
self.t1_screenshot_directory: Optional[Path] = None
self.t1_screenshot_counter = 0
@ -413,6 +446,11 @@ class DebugLink:
self.screen_text_file: Optional[Path] = None
self.last_screen_content = ""
self.waiting_for_layout_change = False
self.layout_dirty = True
self.input_wait_type = DebugWaitType.IMMEDIATE
@property
def legacy_ui(self) -> bool:
"""Differences between UI1 and UI2."""
@ -439,7 +477,12 @@ class DebugLink:
def close(self) -> None:
self.transport.end_session()
def _call(self, msg: protobuf.MessageType, nowait: bool = False) -> Any:
def _write(self, msg: protobuf.MessageType) -> None:
if self.waiting_for_layout_change:
raise RuntimeError(
"Debuglink is unavailable while waiting for layout change."
)
LOG.debug(
f"sending message: {msg.__class__.__name__}",
extra={"protobuf": msg},
@ -450,13 +493,12 @@ class DebugLink:
f"encoded as type {msg_type} ({len(msg_bytes)} bytes): {msg_bytes.hex()}",
)
self.transport.write(msg_type, msg_bytes)
if nowait:
return None
def _read(self) -> protobuf.MessageType:
ret_type, ret_bytes = self.transport.read()
LOG.log(
DUMP_BYTES,
f"received type {msg_type} ({len(msg_bytes)} bytes): {msg_bytes.hex()}",
f"received type {ret_type} ({len(ret_bytes)} bytes): {ret_bytes.hex()}",
)
msg = self.mapping.decode(ret_type, ret_bytes)
@ -472,11 +514,20 @@ class DebugLink:
)
return msg
def state(self) -> messages.DebugLinkState:
return self._call(messages.DebugLinkGetState())
def _call(self, msg: protobuf.MessageType) -> Any:
self._write(msg)
return self._read()
def state(
self, wait_type: DebugWaitType = DebugWaitType.CURRENT_LAYOUT
) -> messages.DebugLinkState:
result = self._call(messages.DebugLinkGetState(wait_layout=wait_type))
if isinstance(result, messages.Failure):
raise TrezorFailure(result)
return result
def read_layout(self) -> LayoutContent:
return LayoutContent(self.state().tokens or [])
return LayoutContent(self.state().tokens)
def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent:
# Next layout change will be caused by external event
@ -487,11 +538,38 @@ class DebugLink:
if wait_for_external_change:
self.reset_debug_events()
obj = self._call(messages.DebugLinkGetState(wait_layout=True))
obj = self._call(
messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT)
)
self.layout_dirty = True
if isinstance(obj, messages.Failure):
raise TrezorFailure(obj)
return LayoutContent(obj.tokens)
@contextmanager
def wait_for_layout_change(self) -> Iterator[LayoutContent]:
# set up a dummy layout content object to be yielded
layout_content = LayoutContent(
["DUMMY CONTENT, WAIT UNTIL THE END OF THE BLOCK :("]
)
# send GetState without waiting for reply
self._write(messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT))
# allow the block to proceed
self.waiting_for_layout_change = True
try:
yield layout_content
finally:
self.waiting_for_layout_change = False
# wait for the reply
resp = self._read()
assert isinstance(resp, messages.DebugLinkState)
# replace contents of the yielded object with the new thing
layout_content.__init__(resp.tokens)
def reset_debug_events(self) -> None:
# Only supported on TT and above certain version
if (self.model is not models.T1B1) and not self.legacy_debug:
@ -535,56 +613,103 @@ class DebugLink:
state = self._call(messages.DebugLinkGetState(wait_word_list=True))
return state.reset_word
def input(
self,
word: Optional[str] = None,
button: Optional[messages.DebugButton] = None,
physical_button: Optional[messages.DebugPhysicalButton] = None,
swipe: Optional[messages.DebugSwipeDirection] = None,
x: Optional[int] = None,
y: Optional[int] = None,
wait: Optional[bool] = None,
hold_ms: Optional[int] = None,
) -> Optional[LayoutContent]:
def _decision(
self, decision: messages.DebugLinkDecision, wait: Optional[bool] = None
) -> LayoutContent:
"""Send a debuglink decision and returns the resulting layout.
If hold_ms is set, an additional 200ms is added to account for processing
delays. (This is needed for hold-to-confirm to trigger reliably.)
If `wait` is unset, the following wait mode is used:
- `IMMEDIATE`, when in normal tests, which never deadlocks the device, but may
return an empty layout in case the next one didn't come up immediately. (E.g.,
in SignTx flow, the device is waiting for more TxRequest/TxAck exchanges
before showing the next UI layout.)
- `CURRENT_LAYOUT`, when in tests running through a `DeviceHandler`. This mode
returns the current layout or waits for some layout to come up if there is
none at the moment. The assumption is that wirelink is communicating on
another thread and won't be blocked by waiting on debuglink.
Force waiting for the layout by setting `wait=True`. Force not waiting by
setting `wait=False` -- useful when, e.g., you are causing the next layout to be
deliberately delayed.
"""
if not self.allow_interactions:
return None
return self.wait_layout()
args = sum(a is not None for a in (word, button, physical_button, swipe, x))
if args != 1:
raise ValueError(
"Invalid input - must use one of word, button, physical_button, swipe, click(x,y)"
)
if decision.hold_ms is not None:
decision.hold_ms += 200
decision = messages.DebugLinkDecision(
button=button,
physical_button=physical_button,
swipe=swipe,
input=word,
x=x,
y=y,
wait=wait,
hold_ms=hold_ms,
self._write(decision)
self.layout_dirty = True
if wait is True:
wait_type = DebugWaitType.CURRENT_LAYOUT
elif wait is False:
wait_type = DebugWaitType.IMMEDIATE
else:
wait_type = self.input_wait_type
return self.snapshot(wait_type)
press_yes = _make_input_func(button=messages.DebugButton.YES)
"""Confirm current layout. See `_decision` for more details."""
press_no = _make_input_func(button=messages.DebugButton.NO)
"""Reject current layout. See `_decision` for more details."""
press_info = _make_input_func(button=messages.DebugButton.INFO)
"""Trigger the Info action. See `_decision` for more details."""
swipe_up = _make_input_func(swipe=messages.DebugSwipeDirection.UP)
"""Swipe up. See `_decision` for more details."""
swipe_down = _make_input_func(swipe=messages.DebugSwipeDirection.DOWN)
"""Swipe down. See `_decision` for more details."""
swipe_right = _make_input_func(swipe=messages.DebugSwipeDirection.RIGHT)
"""Swipe right. See `_decision` for more details."""
swipe_left = _make_input_func(swipe=messages.DebugSwipeDirection.LEFT)
"""Swipe left. See `_decision` for more details."""
press_left = _make_input_func(physical_button=messages.DebugPhysicalButton.LEFT_BTN)
"""Press left button. See `_decision` for more details."""
press_middle = _make_input_func(
physical_button=messages.DebugPhysicalButton.MIDDLE_BTN
)
"""Press middle button. See `_decision` for more details."""
press_right = _make_input_func(
physical_button=messages.DebugPhysicalButton.RIGHT_BTN
)
"""Press right button. See `_decision` for more details."""
def input(self, word: str, wait: Optional[bool] = None) -> LayoutContent:
"""Send text input to the device. See `_decision` for more details."""
return self._decision(messages.DebugLinkDecision(input=word), wait)
def click(
self,
click: Tuple[int, int],
hold_ms: Optional[int] = None,
wait: Optional[bool] = None,
) -> LayoutContent:
"""Send a click to the device. See `_decision` for more details."""
x, y = click
return self._decision(
messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms), wait
)
ret = self._call(decision, nowait=not wait)
if ret is not None:
return LayoutContent(ret.tokens)
def snapshot(
self, wait_type: DebugWaitType = DebugWaitType.IMMEDIATE
) -> LayoutContent:
"""Save text and image content of the screen to relevant directories."""
# take the snapshot
state = self.state(wait_type)
layout = LayoutContent(state.tokens)
# Getting the current screen after the (nowait) decision
self.save_current_screen_if_relevant(wait=False)
if state.tokens and self.layout_dirty:
# save it, unless we already did or unless it's empty
self.save_debug_screen(layout.visible_screen())
if state.layout is not None:
self.save_screenshot(state.layout)
self.layout_dirty = False
return None
def save_current_screen_if_relevant(self, wait: bool = True) -> None:
"""Optionally saving the textual screen output."""
if self.screen_text_file is None:
return
if wait:
layout = self.wait_layout()
else:
layout = self.read_layout()
self.save_debug_screen(layout.visible_screen())
# return the layout
return layout
def save_debug_screen(self, screen_content: str) -> None:
if self.screen_text_file is None:
@ -603,127 +728,8 @@ class DebugLink:
f.write(screen_content)
f.write("\n" + 80 * "/" + "\n")
# Type overloads below make sure that when we supply `wait=True` into functions,
# they will always return `LayoutContent` and we do not need to assert `is not None`.
@overload
def click(self, click: Tuple[int, int]) -> None: ...
@overload
def click(self, click: Tuple[int, int], wait: Literal[True]) -> LayoutContent: ...
def click(
self, click: Tuple[int, int], wait: bool = False
) -> Optional[LayoutContent]:
x, y = click
return self.input(x=x, y=y, wait=wait)
# Made into separate function as `hold_ms: Optional[int]` in `click`
# was causing problems with @overload
def click_hold(
self, click: Tuple[int, int], hold_ms: int
) -> Optional[LayoutContent]:
x, y = click
return self.input(x=x, y=y, hold_ms=hold_ms, wait=True)
def press_yes(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(button=messages.DebugButton.YES, wait=wait)
def press_no(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(button=messages.DebugButton.NO, wait=wait)
def press_info(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(button=messages.DebugButton.INFO, wait=wait)
def swipe_up(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(swipe=messages.DebugSwipeDirection.UP, wait=wait)
def swipe_down(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(swipe=messages.DebugSwipeDirection.DOWN, wait=wait)
@overload
def swipe_right(self) -> None: ...
@overload
def swipe_right(self, wait: Literal[True]) -> LayoutContent: ...
def swipe_right(self, wait: bool = False) -> Union[LayoutContent, None]:
return self.input(swipe=messages.DebugSwipeDirection.RIGHT, wait=wait)
@overload
def swipe_left(self) -> None: ...
@overload
def swipe_left(self, wait: Literal[True]) -> LayoutContent: ...
def swipe_left(self, wait: bool = False) -> Union[LayoutContent, None]:
return self.input(swipe=messages.DebugSwipeDirection.LEFT, wait=wait)
@overload
def press_left(self) -> None: ...
@overload
def press_left(self, wait: Literal[True]) -> LayoutContent: ...
def press_left(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(
physical_button=messages.DebugPhysicalButton.LEFT_BTN, wait=wait
)
@overload
def press_middle(self) -> None: ...
@overload
def press_middle(self, wait: Literal[True]) -> LayoutContent: ...
def press_middle(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(
physical_button=messages.DebugPhysicalButton.MIDDLE_BTN, wait=wait
)
def press_middle_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.MIDDLE_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
@overload
def press_right(self) -> None: ...
@overload
def press_right(self, wait: Literal[True]) -> LayoutContent: ...
def press_right(self, wait: bool = False) -> Optional[LayoutContent]:
return self.input(
physical_button=messages.DebugPhysicalButton.RIGHT_BTN, wait=wait
)
def press_right_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.RIGHT_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
def press_htc(
self, button: messages.DebugPhysicalButton, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
hold_ms = hold_ms + extra_ms # safety margin
result = self.input(
physical_button=button,
hold_ms=hold_ms,
)
# sleeping little longer for UI to update
time.sleep(hold_ms / 1000 + 0.1)
return result
def stop(self) -> None:
self._call(messages.DebugLinkStop(), nowait=True)
self._write(messages.DebugLinkStop())
def reseed(self, value: int) -> protobuf.MessageType:
return self._call(messages.DebugLinkReseedRandom(value=value))
@ -757,44 +763,35 @@ class DebugLink:
return self._call(messages.DebugLinkMemoryRead(address=address, length=length))
def memory_write(self, address: int, memory: bytes, flash: bool = False) -> None:
self._call(
messages.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash),
nowait=True,
self._write(
messages.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash)
)
def flash_erase(self, sector: int) -> None:
self._call(messages.DebugLinkFlashErase(sector=sector), nowait=True)
self._write(messages.DebugLinkFlashErase(sector=sector))
@expect(messages.Success)
def erase_sd_card(self, format: bool = True) -> messages.Success:
return self._call(messages.DebugLinkEraseSdCard(format=format))
def take_t1_screenshot_if_relevant(self) -> None:
"""Conditionally take screenshots on T1.
def save_screenshot(self, data: bytes) -> None:
if self.t1_screenshot_directory is None:
return
TT handles them differently, see debuglink.start_recording.
"""
if self.model is models.T1B1 and self.t1_take_screenshots:
self.save_screenshot_for_t1()
def save_screenshot_for_t1(self) -> None:
from PIL import Image
layout = self.state().layout
assert layout is not None
assert len(layout) == 128 * 64 // 8
assert len(data) == 128 * 64 // 8
pixels: List[int] = []
for byteline in range(64 // 8):
offset = byteline * 128
row = layout[offset : offset + 128]
row = data[offset : offset + 128]
for bit in range(8):
pixels.extend(bool(px & (1 << bit)) for px in row)
im = Image.new("1", (128, 64))
im.putdata(pixels[::-1])
assert self.t1_screenshot_directory is not None
img_location = (
self.t1_screenshot_directory / f"{self.t1_screenshot_counter:04d}.png"
)
@ -802,6 +799,9 @@ class DebugLink:
self.t1_screenshot_counter += 1
del _make_input_func
class NullDebugLink(DebugLink):
def __init__(self) -> None:
# Ignoring type error as self.transport will not be touched while using NullDebugLink
@ -859,15 +859,9 @@ class DebugUI:
self.debuglink.press_yes()
def button_request(self, br: messages.ButtonRequest) -> None:
self.debuglink.take_t1_screenshot_if_relevant()
self.debuglink.snapshot()
if self.input_flow is None:
# Only calling screen-saver when not in input-flow
# as it collides with wait-layout of input flows.
# All input flows call debuglink.input(), so
# recording their screens that way (as well as
# possible swipes below).
self.debuglink.save_current_screen_if_relevant(wait=True)
self._default_input_flow(br)
elif self.input_flow is self.INPUT_FLOW_DONE:
raise AssertionError("input flow ended prematurely")
@ -879,7 +873,7 @@ class DebugUI:
self.input_flow = self.INPUT_FLOW_DONE
def get_pin(self, code: Optional["PinMatrixRequestType"] = None) -> str:
self.debuglink.take_t1_screenshot_if_relevant()
self.debuglink.snapshot()
if self.pins is None:
raise RuntimeError("PIN requested but no sequence was configured")
@ -890,7 +884,7 @@ class DebugUI:
raise AssertionError("PIN sequence ended prematurely")
def get_passphrase(self, available_on_device: bool) -> str:
self.debuglink.take_t1_screenshot_if_relevant()
self.debuglink.snapshot()
return self.passphrase

View File

@ -308,6 +308,12 @@ class DebugPhysicalButton(IntEnum):
RIGHT_BTN = 2
class DebugWaitType(IntEnum):
IMMEDIATE = 0
NEXT_LAYOUT = 1
CURRENT_LAYOUT = 2
class EthereumDefinitionType(IntEnum):
NETWORK = 0
TOKEN = 1
@ -4078,7 +4084,7 @@ class DebugLinkGetState(protobuf.MessageType):
FIELDS = {
1: protobuf.Field("wait_word_list", "bool", repeated=False, required=False, default=None),
2: protobuf.Field("wait_word_pos", "bool", repeated=False, required=False, default=None),
3: protobuf.Field("wait_layout", "bool", repeated=False, required=False, default=None),
3: protobuf.Field("wait_layout", "DebugWaitType", repeated=False, required=False, default=DebugWaitType.IMMEDIATE),
}
def __init__(
@ -4086,7 +4092,7 @@ class DebugLinkGetState(protobuf.MessageType):
*,
wait_word_list: Optional["bool"] = None,
wait_word_pos: Optional["bool"] = None,
wait_layout: Optional["bool"] = None,
wait_layout: Optional["DebugWaitType"] = DebugWaitType.IMMEDIATE,
) -> None:
self.wait_word_list = wait_word_list
self.wait_word_pos = wait_word_pos

View File

@ -546,12 +546,17 @@ def format_message(
return printable / len(bytes) > 0.8
def pformat(name: str, value: t.Any, indent: int) -> str:
from . import messages
level = sep * indent
leadin = sep * (indent + 1)
if isinstance(value, MessageType):
return format_message(value, indent, sep)
if isinstance(pb, messages.DebugLinkState) and name == "tokens":
return "".join(value)
if isinstance(value, list):
# short list of simple values
if not value or all(isinstance(x, int) for x in value):

View File

@ -1127,7 +1127,7 @@ pub struct DebugLinkGetState {
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.wait_word_pos)
pub wait_word_pos: ::std::option::Option<bool>,
// @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.wait_layout)
pub wait_layout: ::std::option::Option<bool>,
pub wait_layout: ::std::option::Option<::protobuf::EnumOrUnknown<debug_link_get_state::DebugWaitType>>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.debug.DebugLinkGetState.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@ -1182,10 +1182,13 @@ impl DebugLinkGetState {
self.wait_word_pos = ::std::option::Option::Some(v);
}
// optional bool wait_layout = 3;
// optional .hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType wait_layout = 3;
pub fn wait_layout(&self) -> bool {
self.wait_layout.unwrap_or(false)
pub fn wait_layout(&self) -> debug_link_get_state::DebugWaitType {
match self.wait_layout {
Some(e) => e.enum_value_or(debug_link_get_state::DebugWaitType::IMMEDIATE),
None => debug_link_get_state::DebugWaitType::IMMEDIATE,
}
}
pub fn clear_wait_layout(&mut self) {
@ -1197,8 +1200,8 @@ impl DebugLinkGetState {
}
// Param is passed by value, moved
pub fn set_wait_layout(&mut self, v: bool) {
self.wait_layout = ::std::option::Option::Some(v);
pub fn set_wait_layout(&mut self, v: debug_link_get_state::DebugWaitType) {
self.wait_layout = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v));
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
@ -1244,7 +1247,7 @@ impl ::protobuf::Message for DebugLinkGetState {
self.wait_word_pos = ::std::option::Option::Some(is.read_bool()?);
},
24 => {
self.wait_layout = ::std::option::Option::Some(is.read_bool()?);
self.wait_layout = ::std::option::Option::Some(is.read_enum_or_unknown()?);
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
@ -1265,7 +1268,7 @@ impl ::protobuf::Message for DebugLinkGetState {
my_size += 1 + 1;
}
if let Some(v) = self.wait_layout {
my_size += 1 + 1;
my_size += ::protobuf::rt::int32_size(3, v.value());
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
@ -1280,7 +1283,7 @@ impl ::protobuf::Message for DebugLinkGetState {
os.write_bool(2, v)?;
}
if let Some(v) = self.wait_layout {
os.write_bool(3, v)?;
os.write_enum(3, ::protobuf::EnumOrUnknown::value(&v))?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
@ -1333,6 +1336,76 @@ impl ::protobuf::reflect::ProtobufValue for DebugLinkGetState {
type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
}
/// Nested message and enums of message `DebugLinkGetState`
pub mod debug_link_get_state {
#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)]
// @@protoc_insertion_point(enum:hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType)
pub enum DebugWaitType {
// @@protoc_insertion_point(enum_value:hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType.IMMEDIATE)
IMMEDIATE = 0,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType.NEXT_LAYOUT)
NEXT_LAYOUT = 1,
// @@protoc_insertion_point(enum_value:hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType.CURRENT_LAYOUT)
CURRENT_LAYOUT = 2,
}
impl ::protobuf::Enum for DebugWaitType {
const NAME: &'static str = "DebugWaitType";
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<DebugWaitType> {
match value {
0 => ::std::option::Option::Some(DebugWaitType::IMMEDIATE),
1 => ::std::option::Option::Some(DebugWaitType::NEXT_LAYOUT),
2 => ::std::option::Option::Some(DebugWaitType::CURRENT_LAYOUT),
_ => ::std::option::Option::None
}
}
fn from_str(str: &str) -> ::std::option::Option<DebugWaitType> {
match str {
"IMMEDIATE" => ::std::option::Option::Some(DebugWaitType::IMMEDIATE),
"NEXT_LAYOUT" => ::std::option::Option::Some(DebugWaitType::NEXT_LAYOUT),
"CURRENT_LAYOUT" => ::std::option::Option::Some(DebugWaitType::CURRENT_LAYOUT),
_ => ::std::option::Option::None
}
}
const VALUES: &'static [DebugWaitType] = &[
DebugWaitType::IMMEDIATE,
DebugWaitType::NEXT_LAYOUT,
DebugWaitType::CURRENT_LAYOUT,
];
}
impl ::protobuf::EnumFull for DebugWaitType {
fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new();
descriptor.get(|| super::file_descriptor().enum_by_package_relative_name("DebugLinkGetState.DebugWaitType").unwrap()).clone()
}
fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor {
let index = *self as usize;
Self::enum_descriptor().value_by_index(index)
}
}
impl ::std::default::Default for DebugWaitType {
fn default() -> Self {
DebugWaitType::IMMEDIATE
}
}
impl DebugWaitType {
pub(in super) fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData {
::protobuf::reflect::GeneratedEnumDescriptorData::new::<DebugWaitType>("DebugLinkGetState.DebugWaitType")
}
}
}
// @@protoc_insertion_point(message:hw.trezor.messages.debug.DebugLinkState)
#[derive(PartialEq,Clone,Default,Debug)]
pub struct DebugLinkState {
@ -3560,53 +3633,56 @@ impl ::protobuf::reflect::ProtobufValue for DebugLinkOptigaSetSecMax {
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x14messages-debug.proto\x12\x18hw.trezor.messages.debug\x1a\x15messag\
es-common.proto\x1a\x19messages-management.proto\x1a\roptions.proto\"\
\xb0\x04\n\x11DebugLinkDecision\x12O\n\x06button\x18\x01\x20\x01(\x0e27.\
\xb4\x04\n\x11DebugLinkDecision\x12O\n\x06button\x18\x01\x20\x01(\x0e27.\
hw.trezor.messages.debug.DebugLinkDecision.DebugButtonR\x06button\x12U\n\
\x05swipe\x18\x02\x20\x01(\x0e2?.hw.trezor.messages.debug.DebugLinkDecis\
ion.DebugSwipeDirectionR\x05swipe\x12\x14\n\x05input\x18\x03\x20\x01(\tR\
\x05input\x12\x0c\n\x01x\x18\x04\x20\x01(\rR\x01x\x12\x0c\n\x01y\x18\x05\
\x20\x01(\rR\x01y\x12\x12\n\x04wait\x18\x06\x20\x01(\x08R\x04wait\x12\
\x17\n\x07hold_ms\x18\x07\x20\x01(\rR\x06holdMs\x12h\n\x0fphysical_butto\
n\x18\x08\x20\x01(\x0e2?.hw.trezor.messages.debug.DebugLinkDecision.Debu\
gPhysicalButtonR\x0ephysicalButton\"<\n\x13DebugSwipeDirection\x12\x06\n\
\x02UP\x10\0\x12\x08\n\x04DOWN\x10\x01\x12\x08\n\x04LEFT\x10\x02\x12\t\n\
\x05RIGHT\x10\x03\"(\n\x0bDebugButton\x12\x06\n\x02NO\x10\0\x12\x07\n\
\x03YES\x10\x01\x12\x08\n\x04INFO\x10\x02\"B\n\x13DebugPhysicalButton\
\x12\x0c\n\x08LEFT_BTN\x10\0\x12\x0e\n\nMIDDLE_BTN\x10\x01\x12\r\n\tRIGH\
T_BTN\x10\x02\")\n\x0fDebugLinkLayout\x12\x16\n\x06tokens\x18\x01\x20\
\x03(\tR\x06tokens\"-\n\x15DebugLinkReseedRandom\x12\x14\n\x05value\x18\
\x01\x20\x01(\rR\x05value\"j\n\x15DebugLinkRecordScreen\x12)\n\x10target\
_directory\x18\x01\x20\x01(\tR\x0ftargetDirectory\x12&\n\rrefresh_index\
\x18\x02\x20\x01(\r:\x010R\x0crefreshIndex\"~\n\x11DebugLinkGetState\x12\
$\n\x0ewait_word_list\x18\x01\x20\x01(\x08R\x0cwaitWordList\x12\"\n\rwai\
t_word_pos\x18\x02\x20\x01(\x08R\x0bwaitWordPos\x12\x1f\n\x0bwait_layout\
\x18\x03\x20\x01(\x08R\nwaitLayout\"\x97\x04\n\x0eDebugLinkState\x12\x16\
\n\x06layout\x18\x01\x20\x01(\x0cR\x06layout\x12\x10\n\x03pin\x18\x02\
\x20\x01(\tR\x03pin\x12\x16\n\x06matrix\x18\x03\x20\x01(\tR\x06matrix\
\x12'\n\x0fmnemonic_secret\x18\x04\x20\x01(\x0cR\x0emnemonicSecret\x129\
\n\x04node\x18\x05\x20\x01(\x0b2%.hw.trezor.messages.common.HDNodeTypeR\
\x04node\x123\n\x15passphrase_protection\x18\x06\x20\x01(\x08R\x14passph\
raseProtection\x12\x1d\n\nreset_word\x18\x07\x20\x01(\tR\tresetWord\x12#\
\n\rreset_entropy\x18\x08\x20\x01(\x0cR\x0cresetEntropy\x12,\n\x12recove\
ry_fake_word\x18\t\x20\x01(\tR\x10recoveryFakeWord\x12*\n\x11recovery_wo\
rd_pos\x18\n\x20\x01(\rR\x0frecoveryWordPos\x12$\n\x0ereset_word_pos\x18\
\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\rmnemonic_type\x18\x0c\x20\x01(\
\x0e2).hw.trezor.messages.management.BackupTypeR\x0cmnemonicType\x12\x16\
\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\"\x0f\n\rDebugLinkStop\"P\n\x0c\
DebugLinkLog\x12\x14\n\x05level\x18\x01\x20\x01(\rR\x05level\x12\x16\n\
\x06bucket\x18\x02\x20\x01(\tR\x06bucket\x12\x12\n\x04text\x18\x03\x20\
\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRead\x12\x18\n\x07address\x18\
\x01\x20\x01(\rR\x07address\x12\x16\n\x06length\x18\x02\x20\x01(\rR\x06l\
ength\")\n\x0fDebugLinkMemory\x12\x16\n\x06memory\x18\x01\x20\x01(\x0cR\
\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\x18\n\x07address\x18\x01\x20\
\x01(\rR\x07address\x12\x16\n\x06memory\x18\x02\x20\x01(\x0cR\x06memory\
\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\x05flash\"-\n\x13DebugLinkFlas\
hErase\x12\x16\n\x06sector\x18\x01\x20\x01(\rR\x06sector\".\n\x14DebugLi\
nkEraseSdCard\x12\x16\n\x06format\x18\x01\x20\x01(\x08R\x06format\",\n\
\x14DebugLinkWatchLayout\x12\x14\n\x05watch\x18\x01\x20\x01(\x08R\x05wat\
ch\"\x1b\n\x19DebugLinkResetDebugEvents\"\x1a\n\x18DebugLinkOptigaSetSec\
MaxB=\n#com.satoshilabs.trezor.lib.protobufB\x12TrezorMessageDebug\x80\
\xa6\x1d\x01\
\x20\x01(\rR\x01y\x12\x16\n\x04wait\x18\x06\x20\x01(\x08R\x04waitB\x02\
\x18\x01\x12\x17\n\x07hold_ms\x18\x07\x20\x01(\rR\x06holdMs\x12h\n\x0fph\
ysical_button\x18\x08\x20\x01(\x0e2?.hw.trezor.messages.debug.DebugLinkD\
ecision.DebugPhysicalButtonR\x0ephysicalButton\"<\n\x13DebugSwipeDirecti\
on\x12\x06\n\x02UP\x10\0\x12\x08\n\x04DOWN\x10\x01\x12\x08\n\x04LEFT\x10\
\x02\x12\t\n\x05RIGHT\x10\x03\"(\n\x0bDebugButton\x12\x06\n\x02NO\x10\0\
\x12\x07\n\x03YES\x10\x01\x12\x08\n\x04INFO\x10\x02\"B\n\x13DebugPhysica\
lButton\x12\x0c\n\x08LEFT_BTN\x10\0\x12\x0e\n\nMIDDLE_BTN\x10\x01\x12\r\
\n\tRIGHT_BTN\x10\x02\"-\n\x0fDebugLinkLayout\x12\x16\n\x06tokens\x18\
\x01\x20\x03(\tR\x06tokens:\x02\x18\x01\"-\n\x15DebugLinkReseedRandom\
\x12\x14\n\x05value\x18\x01\x20\x01(\rR\x05value\"j\n\x15DebugLinkRecord\
Screen\x12)\n\x10target_directory\x18\x01\x20\x01(\tR\x0ftargetDirectory\
\x12&\n\rrefresh_index\x18\x02\x20\x01(\r:\x010R\x0crefreshIndex\"\x91\
\x02\n\x11DebugLinkGetState\x12(\n\x0ewait_word_list\x18\x01\x20\x01(\
\x08R\x0cwaitWordListB\x02\x18\x01\x12&\n\rwait_word_pos\x18\x02\x20\x01\
(\x08R\x0bwaitWordPosB\x02\x18\x01\x12e\n\x0bwait_layout\x18\x03\x20\x01\
(\x0e29.hw.trezor.messages.debug.DebugLinkGetState.DebugWaitType:\tIMMED\
IATER\nwaitLayout\"C\n\rDebugWaitType\x12\r\n\tIMMEDIATE\x10\0\x12\x0f\n\
\x0bNEXT_LAYOUT\x10\x01\x12\x12\n\x0eCURRENT_LAYOUT\x10\x02\"\x97\x04\n\
\x0eDebugLinkState\x12\x16\n\x06layout\x18\x01\x20\x01(\x0cR\x06layout\
\x12\x10\n\x03pin\x18\x02\x20\x01(\tR\x03pin\x12\x16\n\x06matrix\x18\x03\
\x20\x01(\tR\x06matrix\x12'\n\x0fmnemonic_secret\x18\x04\x20\x01(\x0cR\
\x0emnemonicSecret\x129\n\x04node\x18\x05\x20\x01(\x0b2%.hw.trezor.messa\
ges.common.HDNodeTypeR\x04node\x123\n\x15passphrase_protection\x18\x06\
\x20\x01(\x08R\x14passphraseProtection\x12\x1d\n\nreset_word\x18\x07\x20\
\x01(\tR\tresetWord\x12#\n\rreset_entropy\x18\x08\x20\x01(\x0cR\x0creset\
Entropy\x12,\n\x12recovery_fake_word\x18\t\x20\x01(\tR\x10recoveryFakeWo\
rd\x12*\n\x11recovery_word_pos\x18\n\x20\x01(\rR\x0frecoveryWordPos\x12$\
\n\x0ereset_word_pos\x18\x0b\x20\x01(\rR\x0cresetWordPos\x12N\n\rmnemoni\
c_type\x18\x0c\x20\x01(\x0e2).hw.trezor.messages.management.BackupTypeR\
\x0cmnemonicType\x12\x16\n\x06tokens\x18\r\x20\x03(\tR\x06tokens\"\x0f\n\
\rDebugLinkStop\"P\n\x0cDebugLinkLog\x12\x14\n\x05level\x18\x01\x20\x01(\
\rR\x05level\x12\x16\n\x06bucket\x18\x02\x20\x01(\tR\x06bucket\x12\x12\n\
\x04text\x18\x03\x20\x01(\tR\x04text\"G\n\x13DebugLinkMemoryRead\x12\x18\
\n\x07address\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06length\x18\x02\
\x20\x01(\rR\x06length\")\n\x0fDebugLinkMemory\x12\x16\n\x06memory\x18\
\x01\x20\x01(\x0cR\x06memory\"^\n\x14DebugLinkMemoryWrite\x12\x18\n\x07a\
ddress\x18\x01\x20\x01(\rR\x07address\x12\x16\n\x06memory\x18\x02\x20\
\x01(\x0cR\x06memory\x12\x14\n\x05flash\x18\x03\x20\x01(\x08R\x05flash\"\
-\n\x13DebugLinkFlashErase\x12\x16\n\x06sector\x18\x01\x20\x01(\rR\x06se\
ctor\".\n\x14DebugLinkEraseSdCard\x12\x16\n\x06format\x18\x01\x20\x01(\
\x08R\x06format\"0\n\x14DebugLinkWatchLayout\x12\x14\n\x05watch\x18\x01\
\x20\x01(\x08R\x05watch:\x02\x18\x01\"\x1f\n\x19DebugLinkResetDebugEvent\
s:\x02\x18\x01\"\x1a\n\x18DebugLinkOptigaSetSecMaxB=\n#com.satoshilabs.t\
rezor.lib.protobufB\x12TrezorMessageDebug\x80\xa6\x1d\x01\
";
/// `FileDescriptorProto` object which was a source for this generated file
@ -3644,10 +3720,11 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor {
messages.push(DebugLinkWatchLayout::generated_message_descriptor_data());
messages.push(DebugLinkResetDebugEvents::generated_message_descriptor_data());
messages.push(DebugLinkOptigaSetSecMax::generated_message_descriptor_data());
let mut enums = ::std::vec::Vec::with_capacity(3);
let mut enums = ::std::vec::Vec::with_capacity(4);
enums.push(debug_link_decision::DebugSwipeDirection::generated_enum_descriptor_data());
enums.push(debug_link_decision::DebugButton::generated_enum_descriptor_data());
enums.push(debug_link_decision::DebugPhysicalButton::generated_enum_descriptor_data());
enums.push(debug_link_get_state::DebugWaitType::generated_enum_descriptor_data());
::protobuf::reflect::GeneratedFileDescriptor::new_generated(
file_descriptor_proto(),
deps,

View File

@ -2,6 +2,7 @@ from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING, Any, Callable
from trezorlib.client import PASSPHRASE_ON_DEVICE
from trezorlib.messages import DebugWaitType
from trezorlib.transport import udp
if TYPE_CHECKING:
@ -42,6 +43,7 @@ class BackgroundDeviceHandler:
self.client = client
self.client.ui = NullUI # type: ignore [NullUI is OK UI]
self.client.watch_layout(True)
self.client.debug.input_wait_type = DebugWaitType.CURRENT_LAYOUT
def run(self, function: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
"""Runs some function that interacts with a device.
@ -50,8 +52,14 @@ class BackgroundDeviceHandler:
"""
if self.task is not None:
raise RuntimeError("Wait for previous task first")
self.task = self._pool.submit(function, self.client, *args, **kwargs)
self.debuglink().wait_layout(wait_for_external_change=True)
# make sure we start the wait while a layout is up
# TODO should this be part of "wait_for_layout_change"?
self.debuglink().read_layout()
# from the displayed layout, wait for the first UI change triggered by the
# task running in the background
with self.debuglink().wait_for_layout_change():
self.task = self._pool.submit(function, self.client, *args, **kwargs)
def kill_task(self) -> None:
if self.task is not None:

View File

@ -383,8 +383,8 @@ def test_signmessage_pagination_trailing_newline(client: Client):
[
# expect address confirmation
message_filters.ButtonRequest(code=messages.ButtonRequestType.Other),
# expect a ButtonRequest that does not have pagination set
message_filters.ButtonRequest(pages=None),
# expect a ButtonRequest for a single-page screen
message_filters.ButtonRequest(pages=1),
messages.MessageSignature,
]
)