From 73019d079ddbca1fe19ae4b8d17f8b945a434c9e Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 4 Jul 2024 09:32:59 +0200 Subject: [PATCH] feat(core): unify RustLayout, implement single global layout --- common/protob/messages-debug.proto | 28 +- core/src/all_modules.py | 6 +- core/src/apps/bitcoin/sign_tx/layout.py | 3 +- core/src/apps/bitcoin/sign_tx/progress.py | 4 - core/src/apps/common/mnemonic.py | 2 +- core/src/apps/debug/__init__.py | 486 +++++--- core/src/apps/homescreen/__init__.py | 6 +- core/src/apps/management/change_language.py | 2 +- .../apps/management/recovery_device/layout.py | 10 +- core/src/apps/misc/get_firmware_hash.py | 19 +- core/src/apps/webauthn/fido2.py | 34 +- core/src/boot.py | 2 +- core/src/storage/debug.py | 24 +- core/src/trezor/enums/DebugWaitType.py | 7 + core/src/trezor/enums/__init__.py | 5 + core/src/trezor/loop.py | 21 - core/src/trezor/messages.py | 45 +- core/src/trezor/pin.py | 2 +- core/src/trezor/ui/__init__.py | 391 ++++-- core/src/trezor/ui/layouts/common.py | 56 +- core/src/trezor/ui/layouts/homescreen.py | 138 ++- .../src/trezor/ui/layouts/mercury/recovery.py | 32 +- core/src/trezor/ui/layouts/mercury/reset.py | 56 +- core/src/trezor/ui/layouts/progress.py | 54 +- core/src/trezor/ui/layouts/tr/__init__.py | 917 ++++---------- core/src/trezor/ui/layouts/tr/fido.py | 35 +- core/src/trezor/ui/layouts/tr/homescreen.py | 128 -- core/src/trezor/ui/layouts/tr/recovery.py | 121 +- core/src/trezor/ui/layouts/tr/reset.py | 84 +- core/src/trezor/ui/layouts/tt/__init__.py | 1061 ++++++----------- core/src/trezor/ui/layouts/tt/fido.py | 76 +- core/src/trezor/ui/layouts/tt/homescreen.py | 143 --- core/src/trezor/ui/layouts/tt/recovery.py | 178 ++- core/src/trezor/ui/layouts/tt/reset.py | 156 ++- python/src/trezorlib/debuglink.py | 393 +++--- python/src/trezorlib/messages.py | 10 +- python/src/trezorlib/protobuf.py | 5 + .../src/protos/generated/messages_debug.rs | 183 ++- tests/device_handler.py | 12 +- .../device_tests/bitcoin/test_signmessage.py | 4 +- 40 files changed, 2146 insertions(+), 2793 deletions(-) create mode 100644 core/src/trezor/enums/DebugWaitType.py delete mode 100644 core/src/trezor/ui/layouts/tr/homescreen.py delete mode 100644 core/src/trezor/ui/layouts/tt/homescreen.py diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index bdac48b0c..0edfd4979 100644 --- a/common/protob/messages-debug.proto +++ b/common/protob/messages-debug.proto @@ -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; } diff --git a/core/src/all_modules.py b/core/src/all_modules.py index f76f8e798..601f61543 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -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 @@ -183,8 +185,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 +193,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 diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py index cf4044cc8..fedbaba33 100644 --- a/core/src/apps/bitcoin/sign_tx/layout.py +++ b/core/src/apps/bitcoin/sign_tx/layout.py @@ -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), diff --git a/core/src/apps/bitcoin/sign_tx/progress.py b/core/src/apps/bitcoin/sign_tx/progress.py index c375240c2..f435afdde 100644 --- a/core/src/apps/bitcoin/sign_tx/progress.py +++ b/core/src/apps/bitcoin/sign_tx/progress.py @@ -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) diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index e9d9d0ad0..07f669237 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -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]: diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 3f08be81a..c16d3d5f8 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -8,16 +8,15 @@ if __debug__: 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 +24,21 @@ 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_chan = loop.mailbox() DEBUG_CONTEXT: context.Context | None = None - LAYOUT_WATCHER_NONE = 0 - LAYOUT_WATCHER_STATE = 1 - LAYOUT_WATCHER_LAYOUT = 2 - REFRESH_INDEX = 0 + _DEADLOCK_DETECT_SLEEP = loop.sleep(2000) + def screenshot() -> bool: if storage.save_screen: # Starting with "refresh00", allowing for 100 emulator restarts @@ -62,165 +49,236 @@ 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_chan.put(layout, replace=True) - async def _dispatch_debuglink_decision( - event_id: int | None, msg: DebugLinkDecision + def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-is-generator] + while ui.CURRENT_LAYOUT is None: + yield + + 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_chan.is_empty(): + # short-circuit if there is a result already waiting + next_layout = await layout_change_chan + else: + next_layout = await loop.race( + layout_change_chan, _DEADLOCK_DETECT_SLEEP + ) + + if next_layout is None: + # layout close event. loop again + continue + + if isinstance(next_layout, ui.Layout): + break + + if isinstance(next_layout, int): + # sleep result from the deadlock detector + raise wire.FirmwareError("layout deadlock detected") + + raise RuntimeError( + f"Unexpected layout change: {next_layout}, {type(next_layout)}" + ) + + 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) + msg = ui.CURRENT_LAYOUT.layout.touch_event(io.TOUCH_START, x, y) + ui.CURRENT_LAYOUT._emit_message(msg) + ui.CURRENT_LAYOUT._paint() + + if hold_ms: + await loop.sleep(hold_ms) + workflow.idle_timer.touch() + + if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): + return + msg = ui.CURRENT_LAYOUT.layout.touch_event(io.TOUCH_END, x, y) + ui.CURRENT_LAYOUT._emit_message(msg) + ui.CURRENT_LAYOUT._paint() + + 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: + msg = ui.CURRENT_LAYOUT.layout.button_event(io.BUTTON_PRESSED, btn) + ui.CURRENT_LAYOUT._emit_message(msg) + ui.CURRENT_LAYOUT._paint() + + if hold_ms: + await loop.sleep(hold_ms) + workflow.idle_timer.touch() + + if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): + return + for btn in buttons: + msg = ui.CURRENT_LAYOUT.layout.button_event(io.BUTTON_RELEASED, btn) + ui.CURRENT_LAYOUT._emit_message(msg) + ui.CURRENT_LAYOUT._paint() + + 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), + ): + msg = ui.CURRENT_LAYOUT.layout.touch_event(event, x, y) + ui.CURRENT_LAYOUT._emit_message(msg) + ui.CURRENT_LAYOUT._paint() + + 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: - content_tokens = await get_layout_change_content() - - assert isinstance(DEBUG_CONTEXT, context.Context) - 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_chan.clear() - # Touchscreen devices click on specific coordinates, with possible hold - if ( - x is not None - and y is not None - and utils.INTERNAL_MODEL 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 in ("T2B1",): - 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_chan + 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_chan.clear() + return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=False) + + # default behavior: msg.wait_layout == DebugWaitType.CURRENT_LAYOUT + if not isinstance(ui.CURRENT_LAYOUT, ui.Layout): + 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. @@ -228,6 +286,10 @@ if __debug__: REFRESH_INDEX = msg.refresh_index storage.save_screen_directory = msg.target_directory storage.save_screen = True + + # save the initial screenshot (typically homescreen) + screenshot() + else: storage.save_screen = False display.clear_save() # clear C buffers @@ -263,26 +325,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: @@ -293,3 +335,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)) diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index 3f29f1b42..4dcd7646e 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -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, diff --git a/core/src/apps/management/change_language.py b/core/src/apps/management/change_language.py index 88c8dcbf4..bd0fe0b34 100644 --- a/core/src/apps/management/change_language.py +++ b/core/src/apps/management/change_language.py @@ -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) diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index 7de6c75bc..7b29b95e6 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -12,20 +12,18 @@ from trezor.ui.layouts.recovery import ( # noqa: F401 from apps.common import backup_types if TYPE_CHECKING: - from typing import Callable - from trezor.enums import BackupType + from trezor.ui.layouts.common import InfoFunc 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 # Allowing to go back to previous words, therefore cannot use just loop over range(word_count) words: list[str] = [""] * word_count @@ -40,8 +38,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 # User has decided to go back if not word: @@ -122,7 +122,7 @@ async def homescreen_dialog( button_label: str, text: str, subtext: str | None = None, - info_func: Callable | None = None, + info_func: InfoFunc | None = None, show_info: bool = False, ) -> None: import storage.recovery as storage_recovery diff --git a/core/src/apps/misc/get_firmware_hash.py b/core/src/apps/misc/get_firmware_hash.py index 9f40a1572..67a01ba53 100644 --- a/core/src/apps/misc/get_firmware_hash.py +++ b/core/src/apps/misc/get_firmware_hash.py @@ -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) diff --git a/core/src/apps/webauthn/fido2.py b/core/src/apps/webauthn/fido2.py index 056dc56f5..ab712e1d1 100644 --- a/core/src/apps/webauthn/fido2.py +++ b/core/src/apps/webauthn/fido2.py @@ -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 diff --git a/core/src/boot.py b/core/src/boot.py index 9e3086a83..2fcf1cfbb 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -65,7 +65,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() diff --git a/core/src/storage/debug.py b/core/src/storage/debug.py index a8064294a..e674ee92e 100644 --- a/core/src/storage/debug.py +++ b/core/src/storage/debug.py @@ -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 diff --git a/core/src/trezor/enums/DebugWaitType.py b/core/src/trezor/enums/DebugWaitType.py new file mode 100644 index 000000000..3c650960c --- /dev/null +++ b/core/src/trezor/enums/DebugWaitType.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +IMMEDIATE = 0 +NEXT_LAYOUT = 1 +CURRENT_LAYOUT = 2 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 5c3061b7b..9db67a3b7 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -509,6 +509,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 diff --git a/core/src/trezor/loop.py b/core/src/trezor/loop.py index 9ed03ac1a..61dfa9ea0 100644 --- a/core/src/trezor/loop.py +++ b/core/src/trezor/loop.py @@ -669,24 +669,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)) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index f86a88c90..85b06bcf0 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -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 @@ -2761,7 +2762,6 @@ if TYPE_CHECKING: input: "str | None" x: "int | None" y: "int | None" - wait: "bool | None" hold_ms: "int | None" physical_button: "DebugPhysicalButton | None" @@ -2773,7 +2773,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: @@ -2783,20 +2782,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" @@ -2828,16 +2813,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 @@ -2983,26 +2964,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 diff --git a/core/src/trezor/pin.py b/core/src/trezor/pin.py index 27bbd0486..bc3b113d4 100644 --- a/core/src/trezor/pin.py +++ b/core/src/trezor/pin.py @@ -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 diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 7f7c6bf63..16454b081 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -1,21 +1,23 @@ # 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 trezor import io, loop, utils, workflow from trezorui2 import AttachType, BacklightLevels 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 LayoutObj, UiResult # noqa: F401 - T = TypeVar("T") + T = TypeVar("T", covariant=True) else: - Generic = [object] T = 0 + Generic = {T: object} + # all rendering is done through a singleton of `Display` display = Display() @@ -28,15 +30,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 +101,304 @@ 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 + CURRENT_LAYOUT = layout + if __debug__ and not isinstance(layout, ProgressLayout): + from apps.debug import notify_layout_change -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. - """ + notify_layout_change(layout) class Layout(Generic[T]): - """ - Abstract class. + """Python-side handler and runner for the Rust based layouts. - 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. + 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. """ - 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 + BACKLIGHT_LEVEL = BacklightLevels.NORMAL 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]})" + + 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.transition_out: AttachType | None = None + + 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 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 + set_current_layout(self) + + # attach a timer callback and paint self + self.layout.attach_timer_fn(self._set_timer, transition_in) + self._first_paint() + + # spawn all tasks + for task in self.create_tasks(): + self.tasks.add(task) + loop.schedule(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) + + 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: + 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 _paint(self) -> None: + """Paint the layout and ensure that homescreen cache is properly invalidated.""" + import storage.cache as storage_cache + + painted = self.layout.paint() + 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, deadline: int) -> None: + """Timer callback for Rust layouts.""" + + async def timer_task() -> None: + self.timers.pop(token) + result = self.layout.timer(token) + self._paint() + if result is not None: + self.result_box.put(result) + + 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 + loop.schedule(task, token, 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.""" + if msg is None: + return + + # 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[..., object] + ) -> 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() + msg = event_call(*event) + self._emit_message(msg) + self.layout.paint() + except Shutdown: + return + finally: + touch.close() + + 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 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 + 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 + CURRENT_LAYOUT = self + + self.layout.request_complete_repaint() + self.layout.paint() + backlight_fade(BacklightLevels.NONE) + refresh() + + def stop(self) -> None: + global CURRENT_LAYOUT + + if CURRENT_LAYOUT is self: + CURRENT_LAYOUT = None diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py index 1f17d7d02..abb2379ed 100644 --- a/core/src/trezor/ui/layouts/common.py +++ b/core/src/trezor/ui/layouts/common.py @@ -1,42 +1,60 @@ from typing import TYPE_CHECKING -from trezor import workflow +import trezorui2 +from trezor import ui, 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 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 context.wait(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 [Type cannot be assigned to type "None"] diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py index ef13c57a4..18fe413d3 100644 --- a/core/src/trezor/ui/layouts/homescreen.py +++ b/core/src/trezor/ui/layouts/homescreen.py @@ -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: + 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("!") + 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) + self.layout.paint() + ui.refresh() + + 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 + BACKLIGHT_LEVEL = ui.BacklightLevels.LOW + + def __init__( + self, + label: str | None, + bootscreen: bool = False, + coinjoin_authorized: bool = False, + ) -> None: + self.bootscreen = bootscreen + 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 diff --git a/core/src/trezor/ui/layouts/mercury/recovery.py b/core/src/trezor/ui/layouts/mercury/recovery.py index 5ac7403fd..6a3d720aa 100644 --- a/core/src/trezor/ui/layouts/mercury/recovery.py +++ b/core/src/trezor/ui/layouts/mercury/recovery.py @@ -71,30 +71,24 @@ async def show_remaining_shares( pages.append((title, words)) await raise_if_not_confirmed( - interact( - RustLayout(trezorui2.show_remaining_shares(pages=pages)), - "show_shares", - ButtonRequestType.Other, - ) + 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, - ) + 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, ) diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index d65abcc8d..13e68b4b7 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -106,13 +106,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, @@ -301,25 +299,21 @@ 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_warning", + "backup_intro", 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, - ) + trezorui2.show_warning( + title=TR.words__important, + value=TR.reset__never_make_digital_copy, + button="", + allow_cancel=False, ), "backup_warning", ButtonRequestType.ResetDevice, @@ -342,20 +336,16 @@ async def show_reset_warning( 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, - ) - ), - br_name, - br_code, - ) + await interact( + trezorui2.show_warning( + title=subheader or "", + description=content, + value="", + button="", + allow_cancel=False, + ), + br_name, + br_code, ) diff --git a/core/src/trezor/ui/layouts/progress.py b/core/src/trezor/ui/layouts/progress.py index 497da3d28..ecb9fe94e 100644 --- a/core/src/trezor/ui/layouts/progress.py +++ b/core/src/trezor/ui/layouts/progress.py @@ -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,22 @@ 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, deadline: int) -> None: - raise RuntimeError # progress layouts should not set timers - - def report(self, value: int, description: str | None = None): - msg = self.layout.progress_event(value, description or "") - assert msg is None - 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( + if title is not None: + title = title.upper() + elif utils.MODEL != "T2B1": + # on TT, uppercase the description which ends up on top of the screen + # when no title is set + description = description.upper() + + return ui.ProgressLayout( layout=trezorui2.show_progress( description=description, title=title, @@ -67,27 +45,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) diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 446322fbf..b5401fc60 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -1,26 +1,17 @@ from typing import TYPE_CHECKING import trezorui2 -from trezor import TR, io, log, loop, ui, utils +from trezor import TR, ui, utils from trezor.enums import ButtonRequestType -from trezor.messages import ButtonAck, ButtonRequest -from trezor.wire import ActionCancelled, context +from trezor.wire import ActionCancelled -from ..common import button_request, interact +from ..common import interact, raise_if_not_confirmed if TYPE_CHECKING: - from typing import Any, Awaitable, Iterable, NoReturn, Sequence, TypeVar + from typing import Any, Awaitable, Iterable, NoReturn, Sequence from ..common import ExceptionType, PropertyType - T = TypeVar("T") - - LayoutParentType = ui.Layout[T] - -else: - LayoutParentType = [ui.Layout] - T = 0 - CONFIRMED = trezorui2.CONFIRMED CANCELLED = trezorui2.CANCELLED @@ -35,277 +26,6 @@ if __debug__: trezorui2.disable_animation(bool(DISABLE_ANIMATION)) -class RustLayout(LayoutParentType[T]): - # pylint: disable=super-init-not-called - def __init__(self, layout: trezorui2.LayoutObj[T]): - self.br_chan = loop.chan() - self.layout = layout - self.timer = loop.Timer() - self.layout.attach_timer_fn(self.set_timer, None) - self._send_button_request() - - def __del__(self): - self.layout.__del__() - - def set_timer(self, token: int, deadline: int) -> None: - self.timer.schedule(deadline, token) - - def request_complete_repaint(self) -> None: - msg = self.layout.request_complete_repaint() - assert msg is None - - def _paint(self) -> None: - import storage.cache as storage_cache - - painted = self.layout.paint() - - if painted: - ui.refresh() - if storage_cache.homescreen_shown is not None and painted: - storage_cache.homescreen_shown = None - - if __debug__: - from trezor.enums import DebugPhysicalButton - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - if context.CURRENT_CONTEXT: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_swipe_signal(), - self.handle_button_signal(), - self.handle_result_signal(), - self.handle_usb(context.get_context()), - ) - else: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_swipe_signal(), - self.handle_button_signal(), - self.handle_result_signal(), - ) - - async def handle_result_signal(self) -> None: - """Enables sending arbitrary input - ui.Result. - - Waits for `result_signal` and carries it out. - """ - from storage import debug as debug_storage - - from apps.debug import result_signal - - while True: - event_id, result = await result_signal() - # Layout change will be notified in _first_paint of the next layout - debug_storage.new_layout_event_id = event_id - raise ui.Result(result) - - def read_content_into(self, content_store: list[str]) -> None: - """Reads all the strings/tokens received from Rust into given list.""" - - def callback(*args: Any) -> None: - for arg in args: - content_store.append(str(arg)) - - content_store.clear() - self.layout.trace(callback) - - async def _press_left(self, hold_ms: int | None) -> Any: - """Triggers left button press.""" - self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_LEFT) - self._send_button_request() - self._paint() - if hold_ms is not None: - await loop.sleep(hold_ms) - r = self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_LEFT) - self._send_button_request() - return r - - async def _press_right(self, hold_ms: int | None) -> Any: - """Triggers right button press.""" - self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_RIGHT) - self._send_button_request() - self._paint() - if hold_ms is not None: - await loop.sleep(hold_ms) - r = self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_RIGHT) - self._send_button_request() - return r - - async def _press_middle(self, hold_ms: int | None) -> Any: - """Triggers middle button press.""" - self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_LEFT) - self._send_button_request() - self._paint() - self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_RIGHT) - self._send_button_request() - self._paint() - if hold_ms is not None: - await loop.sleep(hold_ms) - self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_LEFT) - self._send_button_request() - self._paint() - r = self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_RIGHT) - self._send_button_request() - return r - - async def _press_button( - self, - event_id: int | None, - btn_to_press: DebugPhysicalButton, - hold_ms: int | None, - ) -> Any: - from storage import debug as debug_storage - from trezor import workflow - from trezor.enums import DebugPhysicalButton - - from apps.debug import notify_layout_change - - if btn_to_press == DebugPhysicalButton.LEFT_BTN: - msg = await self._press_left(hold_ms) - elif btn_to_press == DebugPhysicalButton.MIDDLE_BTN: - msg = await self._press_middle(hold_ms) - elif btn_to_press == DebugPhysicalButton.RIGHT_BTN: - msg = await self._press_right(hold_ms) - else: - raise Exception(f"Unknown button: {btn_to_press}") - - if msg is not None: - # Layout change will be notified in _first_paint of the next layout - debug_storage.new_layout_event_id = event_id - raise ui.Result(msg) - - # So that these presses will keep trezor awake - # (it will not be locked after auto_lock_delay_ms) - workflow.idle_timer.touch() - - self._paint() - notify_layout_change(self, event_id) - - async def _swipe(self, event_id: int | None, direction: int) -> None: - """Triggers swipe in the given direction. - - Only `UP` and `DOWN` directions are supported. - """ - from trezor.enums import DebugPhysicalButton, DebugSwipeDirection - - if direction == DebugSwipeDirection.UP: - btn_to_press = DebugPhysicalButton.RIGHT_BTN - elif direction == DebugSwipeDirection.DOWN: - btn_to_press = DebugPhysicalButton.LEFT_BTN - else: - raise Exception(f"Unsupported direction: {direction}") - - await self._press_button(event_id, btn_to_press, None) - - async def handle_swipe_signal(self) -> None: - """Enables pagination through the current page/flow page. - - Waits for `swipe_signal` and carries it out. - """ - from apps.debug import swipe_signal - - while True: - event_id, direction = await swipe_signal() - await self._swipe(event_id, direction) - - async def handle_button_signal(self) -> None: - """Enables clicking arbitrary of the three buttons. - - Waits for `button_signal` and carries it out. - """ - from apps.debug import button_signal - - while True: - event_id, btn, hold_ms = await button_signal() - await self._press_button(event_id, btn, hold_ms) - - else: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - if context.CURRENT_CONTEXT: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_usb(context.get_context()), - ) - else: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - ) - - def _first_paint(self) -> None: - self._paint() - - if __debug__ and self.should_notify_layout_change: - from storage import debug as debug_storage - - from apps.debug import notify_layout_change - - # notify about change and do not notify again until next await. - # (handle_rendering might be called multiple times in a single await, - # because of the endless loop in __iter__) - self.should_notify_layout_change = False - - # Possibly there is an event ID that caused the layout change, - # so notifying with this ID. - event_id = None - if debug_storage.new_layout_event_id is not None: - event_id = debug_storage.new_layout_event_id - debug_storage.new_layout_event_id = None - - notify_layout_change(self, event_id) - - def handle_input_and_rendering(self) -> loop.Task: - from trezor import workflow - - button = loop.wait(io.BUTTON) - self._first_paint() - while True: - # Using `yield` instead of `await` to avoid allocations. - event, button_num = yield button - workflow.idle_timer.touch() - msg = None - if event in (io.BUTTON_PRESSED, io.BUTTON_RELEASED): - msg = self.layout.button_event(event, button_num) - self._send_button_request() - if msg is not None: - raise ui.Result(msg) - self._paint() - - def handle_timers(self) -> loop.Task: - while True: - # Using `yield` instead of `await` to avoid allocations. - token = yield self.timer - msg = self.layout.timer(token) - self._send_button_request() - if msg is not None: - raise ui.Result(msg) - self._paint() - - def page_count(self) -> int: - """How many paginated pages current screen has.""" - return self.layout.page_count() - - async def handle_usb(self, ctx: context.Context): - while True: - br_code, br_name, page_count = await loop.race( - ctx.read(()), self.br_chan.take() - ) - log.debug(__name__, "ButtonRequest.name=%s", br_name) - await ctx.call( - ButtonRequest(code=br_code, pages=page_count, name=br_name), ButtonAck - ) - - def _send_button_request(self): - res = self.layout.button_request() - if res is not None: - br_code, br_name = res - self.br_chan.publish((br_code, br_name, self.layout.page_count())) - - def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None: # Simple drawing not supported for layouts that set timers. def dummy_set_timer(token: int, deadline: int) -> None: @@ -343,43 +63,6 @@ def _placeholder_confirm( ) -async def get_bool( - br_name: str, - title: str, - data: str | None = None, - description: str | None = None, - verb: str | None = None, - verb_cancel: str | None = "", - hold: bool = False, - br_code: ButtonRequestType = BR_CODE_OTHER, -) -> bool: - verb = verb or TR.buttons__confirm # def_arg - result = await interact( - RustLayout( - trezorui2.confirm_action( - title=title, - action=data, - description=description, - verb=verb, - verb_cancel=verb_cancel, - hold=hold, - ) - ), - br_name, - br_code, - ) - - return result is CONFIRMED - - -async def raise_if_not_confirmed( - a: Awaitable[ui.UiResult], exc: Any = ActionCancelled -) -> None: - result = await a - if result is not CONFIRMED: - raise exc - - def confirm_action( br_name: str, title: str, @@ -402,22 +85,18 @@ def confirm_action( description = description.format(description_param) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_action( - title=title, - action=action, - description=description, + trezorui2.confirm_action( + title=title, + action=action, + description=description, subtitle=subtitle, - verb=verb, - verb_cancel=verb_cancel, - hold=hold, - reverse=reverse, - ) - ), - br_name, - br_code, + verb=verb, + verb_cancel=verb_cancel, + hold=hold, + reverse=reverse, ), + br_name, + br_code, exc, ) @@ -433,8 +112,6 @@ def confirm_single( # Placeholders are coming from translations in form of {0} template_str = "{0}" - if template_str not in description: - template_str = "{}" begin, _separator, end = description.partition(template_str) return confirm_action( @@ -456,20 +133,12 @@ def confirm_reset_device( button = TR.reset__button_create return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_reset_device( - title=title, - button=button, - ) - ), - "recover_device" if recovery else "setup_device", - ( - ButtonRequestType.ProtectCall - if recovery - else ButtonRequestType.ResetDevice - ), - ) + trezorui2.confirm_reset_device( + title=title, + button=button, + ), + "recover_device" if recovery else "setup_device", + ButtonRequestType.ProtectCall if recovery else ButtonRequestType.ResetDevice, ) @@ -483,21 +152,28 @@ async def prompt_backup() -> bool: br_code = ButtonRequestType.ResetDevice result = await interact( - RustLayout(trezorui2.confirm_backup()), + trezorui2.confirm_backup(), br_name, br_code, + raise_on_cancel=None, ) if result is CONFIRMED: return True - return await get_bool( + result = await interact( + trezorui2.confirm_action( + title=TR.backup__title_skip.upper(), + action=None, + description=TR.backup__want_to_skip, + verb=TR.buttons__back_up, + verb_cancel=TR.buttons__skip, + hold=False, + ), br_name, - TR.backup__title_skip, - description=TR.backup__want_to_skip, - verb=TR.buttons__back_up, - verb_cancel=TR.buttons__skip, - br_code=br_code, + br_code, + raise_on_cancel=None, ) + return result is CONFIRMED def confirm_path_warning( @@ -513,7 +189,7 @@ def confirm_path_warning( ) -def confirm_multisig_warning() -> Awaitable[None]: +def confirm_multisig_warning() -> Awaitable[ui.UiResult]: return show_warning( "warning_multisig", TR.send__receiving_to_multisig, @@ -523,16 +199,12 @@ def confirm_multisig_warning() -> Awaitable[None]: def confirm_homescreen(image: bytes) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_homescreen( - title=TR.homescreen__title_set, - image=image, - ) - ), - "set_homesreen", - ButtonRequestType.ProtectCall, - ) + trezorui2.confirm_homescreen( + title=TR.homescreen__title_set, + image=image, + ), + "set_homesreen", + ButtonRequestType.ProtectCall, ) @@ -558,26 +230,22 @@ async def show_address( # Will be a marquee in case of multisig title = TR.address__title_receive_address if multisig_index is not None: - title = f"{title} (MULTISIG)" + title = f"{title} (MULTISIG)" # TODO translation? + while True: - layout = RustLayout( + result = await interact( trezorui2.confirm_address( title=title, data=address, description="", # unused on TR extra=None, # unused on TR chunkify=chunkify, - ) + ), + br_name if send_button_request else None, + br_code, + raise_on_cancel=None, ) - if send_button_request: - send_button_request = False - await button_request( - br_name, - br_code, - pages=layout.page_count(), - ) - layout.request_complete_repaint() - result = await layout + send_button_request = False # User confirmed with middle button. if result is CONFIRMED: @@ -596,7 +264,7 @@ async def show_address( ) return result - result = await RustLayout( + result = await interact( trezorui2.show_address_details( qr_title="", # unused on this model address=address if address_qr is None else address_qr, @@ -605,14 +273,20 @@ async def show_address( account=account, path=path, xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], - ) + ), + None, + raise_on_cancel=None, ) # Can only go back from the address details. assert result is CANCELLED # User pressed left cancel button, show mismatch dialogue. else: - result = await RustLayout(trezorui2.show_mismatch(title=mismatch_title)) + result = await interact( + trezorui2.show_mismatch(title=mismatch_title.upper()), + None, + raise_on_cancel=None, + ) assert result in (CONFIRMED, CANCELLED) # Right button aborts action, left goes back to showing address. if result is CONFIRMED: @@ -678,7 +352,9 @@ async def show_error_and_raise( content, button=button, br_code=BR_CODE_OTHER, + exc=None, ) + # always raise regardless of result raise exc @@ -688,7 +364,8 @@ def show_warning( subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> Awaitable[None]: + exc: ExceptionType | None = ActionCancelled, +) -> Awaitable[ui.UiResult]: from trezor import translations button = button or TR.buttons__continue # def_arg @@ -701,15 +378,14 @@ def show_warning( content = content + "\n" return interact( - RustLayout( - trezorui2.show_warning( # type: ignore [Argument missing for parameter "title"] - button=button, - warning=content, # type: ignore [No parameter named "warning"] - description=subheader or "", - ) + trezorui2.show_warning( # type: ignore [Argument missing for parameter "title"] + button=button, + warning=content, # type: ignore [No parameter named "warning"] + description=subheader or "", ), br_name, br_code, + raise_on_cancel=exc, ) @@ -767,44 +443,36 @@ async def confirm_output( amount_title += f" #{output_index + 1}" while True: - result = await interact( - RustLayout( - trezorui2.confirm_output_address( - address=address, - address_label=address_label or "", - address_title=address_title, - chunkify=chunkify, - ) + await interact( + trezorui2.confirm_output_address( + address=address, + address_label=address_label or "", + address_title=address_title, + chunkify=chunkify, ), "confirm_output", br_code, ) - if result is not CONFIRMED: - raise ActionCancelled - result = await interact( - RustLayout( + try: + await interact( trezorui2.confirm_output_amount( amount_title=amount_title, amount=amount, - ) - ), - "confirm_output", - br_code, - ) - if result is CONFIRMED: + ), + "confirm_output", + br_code, + ) + except ActionCancelled: + # if the user cancels here, go back to confirm_value + continue + else: return -def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]: +def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[ui.UiResult]: """Showing users how to interact with the device.""" - return raise_if_not_confirmed( - interact( - RustLayout(trezorui2.tutorial()), - "tutorial", - br_code, - ) - ) + return interact(trezorui2.tutorial(), "tutorial", br_code) async def should_show_payment_request_details( @@ -841,14 +509,12 @@ async def should_show_more( confirm = TR.buttons__confirm result = await interact( - RustLayout( - trezorui2.confirm_with_info( - title=title, - items=para, - button=confirm, - verb_cancel=verb_cancel, # type: ignore [No parameter named "verb_cancel"] - info_button=button_text, # unused on TR - ) + trezorui2.confirm_with_info( + title=title, + items=para, + button=confirm, + verb_cancel=verb_cancel, # type: ignore [No parameter named "verb_cancel"] + info_button=button_text, # unused on TR ), br_name, br_code, @@ -859,8 +525,7 @@ async def should_show_more( elif result is INFO: return True else: - assert result is CANCELLED - raise ActionCancelled + raise RuntimeError # ActionCancelled should have been raised by interact() def confirm_blob( @@ -877,17 +542,16 @@ def confirm_blob( prompt_screen: bool = True, ) -> Awaitable[None]: verb = verb or TR.buttons__confirm # def_arg - layout = RustLayout( - trezorui2.confirm_blob( - title=title, - description=description, - data=data, - extra=None, - verb=verb, - verb_cancel=verb_cancel, - hold=hold, - chunkify=chunkify, - ) + description = description or "" + layout = trezorui2.confirm_blob( + title=title, + description=description, + data=data, + extra=None, + verb=verb, + verb_cancel=verb_cancel, + hold=hold, + chunkify=chunkify, ) if ask_pagination and layout.page_count() > 1: @@ -895,15 +559,8 @@ def confirm_blob( return _confirm_ask_pagination( br_name, title, data, description or "", verb_cancel, br_code ) - else: - return raise_if_not_confirmed( - interact( - layout, - br_name, - br_code, - ) - ) + return raise_if_not_confirmed(layout, br_name, br_code) async def _confirm_ask_pagination( @@ -914,12 +571,18 @@ async def _confirm_ask_pagination( verb_cancel: str | None, br_code: ButtonRequestType, ) -> None: - paginated: RustLayout[trezorui2.UiResult] | None = None # TODO: make should_show_more/confirm_more accept bytes directly if isinstance(data, (bytes, bytearray, memoryview)): from ubinascii import hexlify data = hexlify(data).decode() + + confirm_more_layout = trezorui2.confirm_more( + title=title, + button="GO BACK", + items=[(ui.BOLD_UPPER, f"Size: {len(data)} bytes"), (ui.MONO, data)], + ) + while True: if not await should_show_more( title, @@ -930,22 +593,7 @@ async def _confirm_ask_pagination( ): return - if paginated is None: - paginated = RustLayout( - trezorui2.confirm_more( - title=title, - button=TR.buttons__go_back, - items=[ - (ui.BOLD_UPPER, f"Size: {len(data)} bytes"), - (ui.MONO, data), - ], - ) - ) - else: - paginated.request_complete_repaint() - - result = await interact(paginated, br_name, br_code) - assert result in (CONFIRMED, CANCELLED) + await interact(confirm_more_layout, br_name, br_code, raise_on_cancel=None) assert False @@ -1019,17 +667,13 @@ def confirm_properties( return (key, value, is_data) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_properties( - title=title, - items=map(handle_bytes, props), # type: ignore [cannot be assigned to parameter "items"] - hold=hold, - ) - ), - br_name, - br_code, - ) + trezorui2.confirm_properties( + title=title, + items=map(handle_bytes, props), # type: ignore [cannot be assigned to parameter "items"] + hold=hold, + ), + br_name, + br_code, ) @@ -1052,20 +696,17 @@ async def confirm_value( if info_items is None: return await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_value( # type: ignore [Argument missing for parameter "subtitle"] - title=title, - description=description, - value=value, - verb=verb or TR.buttons__hold_to_confirm, - hold=hold, - ) - ), - br_name, - br_code, - ) + trezorui2.confirm_value( # type: ignore [Argument missing for parameter "subtitle"] + title=title, + description=description, + value=value, + verb=verb or TR.buttons__hold_to_confirm, + hold=hold, + ), + br_name, + br_code, ) + else: info_items_list = list(info_items) if len(info_items_list) > 1: @@ -1094,11 +735,11 @@ async def confirm_value( if result is CONFIRMED: return - elif result is INFO: + if result is INFO: info_title, info_value = info_items_list[0] - await RustLayout( + await interact( trezorui2.confirm_blob( - title=info_title, + title=info_title.upper(), data=info_value, description=description, extra=None, @@ -1106,11 +747,12 @@ async def confirm_value( verb_cancel="<", hold=False, chunkify=chunkify_info, - ) + ), + None, + raise_on_cancel=None, ) - else: - assert result is CANCELLED - raise ActionCancelled + continue + raise RuntimeError # unexpected result, interact should have raised def confirm_total( @@ -1127,22 +769,18 @@ def confirm_total( ) -> Awaitable[None]: total_label = total_label or TR.send__total_amount_colon # def_arg fee_label = fee_label or TR.send__including_fee # def_arg - return raise_if_not_confirmed( - interact( - RustLayout( - # TODO: resolve these differences in TT's and TR's confirm_total - trezorui2.confirm_total( # type: ignore [Arguments missing] - total_amount=total_amount, # type: ignore [No parameter named] - fee_amount=fee_amount, # type: ignore [No parameter named] - fee_rate_amount=fee_rate_amount, # type: ignore [No parameter named] - account_label=source_account, # type: ignore [No parameter named] - total_label=total_label, # type: ignore [No parameter named] - fee_label=fee_label, # type: ignore [No parameter named] - ) - ), - br_name, - br_code, - ) + return interact( + # TODO: resolve these differences in TT's and TR's confirm_total + trezorui2.confirm_total( # type: ignore [Arguments missing] + total_amount=total_amount, # type: ignore [No parameter named] + fee_amount=fee_amount, # type: ignore [No parameter named] + fee_rate_amount=fee_rate_amount, # type: ignore [No parameter named] + account_label=source_account, # type: ignore [No parameter named] + total_label=total_label, # type: ignore [No parameter named] + fee_label=fee_label, # type: ignore [No parameter named] + ), + br_name, + br_code, ) @@ -1181,20 +819,16 @@ if not utils.BITCOIN_ONLY: amount_title = TR.words__amount + ":" amount_value = total_amount await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.altcoin_tx_summary( - amount_title=amount_title, - amount_value=amount_value, - fee_title=TR.send__maximum_fee, - fee_value=maximum_fee, - items=info_items, - cancel_cross=True, - ) - ), - br_name=br_name, - br_code=br_code, - ) + trezorui2.altcoin_tx_summary( + amount_title=amount_title, + amount_value=amount_value, + fee_title=TR.send__maximum_fee, + fee_value=maximum_fee, + items=info_items, + cancel_cross=True, + ), + br_name=br_name, + br_code=br_code, ) def confirm_solana_tx( @@ -1211,20 +845,16 @@ if not utils.BITCOIN_ONLY: ) # def_arg fee_title = fee_title or TR.words__fee # def_arg return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.altcoin_tx_summary( - amount_title=amount_title, - amount_value=amount, - fee_title=fee_title, - fee_value=fee, - items=items, - cancel_cross=True, - ) - ), - br_name=br_name, - br_code=br_code, - ) + trezorui2.altcoin_tx_summary( + amount_title=amount_title, + amount_value=amount, + fee_title=fee_title, + fee_value=fee, + items=items, + cancel_cross=True, + ), + br_name=br_name, + br_code=br_code, ) async def confirm_ethereum_tx( @@ -1236,14 +866,12 @@ if not utils.BITCOIN_ONLY: br_code: ButtonRequestType = ButtonRequestType.SignTx, chunkify: bool = False, ) -> None: - summary_layout = RustLayout( - trezorui2.altcoin_tx_summary( - amount_title=f"{TR.words__amount}:", - amount_value=total_amount, - fee_title=TR.send__maximum_fee, - fee_value=maximum_fee, - items=items, - ) + summary_layout = trezorui2.altcoin_tx_summary( + amount_title=f"{TR.words__amount}:", + amount_value=total_amount, + fee_title=TR.send__maximum_fee, + fee_value=maximum_fee, + items=items, ) while True: @@ -1257,13 +885,10 @@ if not utils.BITCOIN_ONLY: ) try: - summary_layout.request_complete_repaint() await raise_if_not_confirmed( - interact( - summary_layout, - br_name, - br_code, - ) + summary_layout, + br_name, + br_code, ) break except ActionCancelled: @@ -1272,16 +897,12 @@ if not utils.BITCOIN_ONLY: def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_joint_total( - spending_amount=spending_amount, - total_amount=total_amount, - ) - ), - "confirm_joint_total", - ButtonRequestType.SignTx, - ) + trezorui2.confirm_joint_total( + spending_amount=spending_amount, + total_amount=total_amount, + ), + "confirm_joint_total", + ButtonRequestType.SignTx, ) @@ -1319,46 +940,38 @@ async def confirm_modify_output( amount_change: str, amount_new: str, ) -> None: - address_layout = RustLayout( - trezorui2.confirm_blob( - title=TR.modify_amount__title, - data=address, - verb=TR.buttons__continue, - verb_cancel=None, - description=f"{TR.words__address}:", - extra=None, - ) + address_layout = trezorui2.confirm_blob( + title=TR.modify_amount__title, + data=address, + verb=TR.buttons__continue, + verb_cancel=None, + description=f"{TR.words__address}:", + extra=None, ) - modify_layout = RustLayout( - trezorui2.confirm_modify_output( - sign=sign, - amount_change=amount_change, - amount_new=amount_new, - ) + + modify_layout = trezorui2.confirm_modify_output( + sign=sign, + amount_change=amount_change, + amount_new=amount_new, ) send_button_request = True while True: - if send_button_request: - await button_request( - "modify_output", + await raise_if_not_confirmed( + address_layout, + "modify_output" if send_button_request else None, + ButtonRequestType.ConfirmOutput, + ) + try: + await raise_if_not_confirmed( + modify_layout, + "modify_output" if send_button_request else None, ButtonRequestType.ConfirmOutput, - address_layout.page_count(), ) - address_layout.request_complete_repaint() - await raise_if_not_confirmed(address_layout) - - if send_button_request: + except ActionCancelled: send_button_request = False - await button_request( - "modify_output", - ButtonRequestType.ConfirmOutput, - modify_layout.page_count(), - ) - modify_layout.request_complete_repaint() - result = await modify_layout - - if result is CONFIRMED: + continue + else: break @@ -1370,34 +983,26 @@ def confirm_modify_fee( fee_rate_amount: str | None = None, ) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_modify_fee( - title=title, - sign=sign, - user_fee_change=user_fee_change, - total_fee_new=total_fee_new, - fee_rate_amount=fee_rate_amount, - ) - ), - "modify_fee", - ButtonRequestType.SignTx, - ) + trezorui2.confirm_modify_fee( + title=title, + sign=sign, + user_fee_change=user_fee_change, + total_fee_new=total_fee_new, + fee_rate_amount=fee_rate_amount, + ), + "modify_fee", + ButtonRequestType.SignTx, ) def confirm_coinjoin(max_rounds: int, max_fee_per_vbyte: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_coinjoin( - max_rounds=str(max_rounds), - max_feerate=max_fee_per_vbyte, - ) - ), - "coinjoin_final", - BR_CODE_OTHER, - ) + trezorui2.confirm_coinjoin( + max_rounds=str(max_rounds), + max_feerate=max_fee_per_vbyte, + ), + "coinjoin_final", + BR_CODE_OTHER, ) @@ -1453,7 +1058,7 @@ async def confirm_signverify( break -def show_error_popup( +def error_popup( title: str, description: str, subtitle: str | None = None, @@ -1461,20 +1066,18 @@ def show_error_popup( *, button: str = "", timeout_ms: int = 0, -) -> Awaitable[None]: +) -> trezorui2.LayoutObj[trezorui2.UiResult]: if button: raise NotImplementedError("Button not implemented") + description = description.format(description_param) if subtitle: description = f"{subtitle}\n{description}" - layout = RustLayout( - trezorui2.show_info( - title=title, - description=description, - time_ms=timeout_ms, - ) + return trezorui2.show_info( + title=title, + description=description, + time_ms=timeout_ms, ) - return layout # type: ignore [Expression of type "RustLayout[UiResult]" is incompatible with return type "Awaitable[None]"] def request_passphrase_on_host() -> None: @@ -1487,18 +1090,14 @@ def show_wait_text(message: str) -> None: async def request_passphrase_on_device(max_len: int) -> str: result = await interact( - RustLayout( - trezorui2.request_passphrase( - prompt=TR.passphrase__title_enter, - max_len=max_len, - ) + trezorui2.request_passphrase( + prompt=TR.passphrase__title_enter, + max_len=max_len, ), "passphrase_device", ButtonRequestType.PassphraseEntry, + raise_on_cancel=ActionCancelled("Passphrase entry cancelled"), ) - if result is CANCELLED: - raise ActionCancelled("Passphrase entry cancelled") - assert isinstance(result, str) return result @@ -1521,20 +1120,17 @@ async def request_pin_on_device( subprompt = f"{attempts_remaining} {TR.pin__tries_left}" result = await interact( - RustLayout( - trezorui2.request_pin( - prompt=prompt, - subprompt=subprompt, - allow_cancel=allow_cancel, - wrong_pin=wrong_pin, - ) + trezorui2.request_pin( + prompt=prompt.upper(), + subprompt=subprompt, + allow_cancel=allow_cancel, + wrong_pin=wrong_pin, ), "pin_device", ButtonRequestType.PinEntry, + raise_on_cancel=wire.PinCancelled, ) - if result is CANCELLED: - raise wire.PinCancelled assert isinstance(result, str) return result @@ -1563,30 +1159,27 @@ def _confirm_multiple_pages_texts( br_code: ButtonRequestType = BR_CODE_OTHER, ) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.multiple_pages_texts( - title=title, - verb=verb, - items=items, - ) - ), - br_name, - br_code, - ) + trezorui2.multiple_pages_texts( + title=title, + verb=verb, + items=items, + ), + br_name, + br_code, ) def pin_mismatch_popup(is_wipe_code: bool = False) -> Awaitable[None]: description = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch br_code = "wipe_code_mismatch" if is_wipe_code else "pin_mismatch" - return show_warning( + layout = show_warning( br_code, description, TR.pin__please_check_again, TR.buttons__check_again, BR_CODE_OTHER, ) + return layout # type: ignore [Type cannot be assigned to type "None"] def wipe_code_same_as_pin_popup() -> Awaitable[None]: @@ -1635,13 +1228,9 @@ async def confirm_set_new_pin( def confirm_firmware_update(description: str, fingerprint: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_firmware_update( - description=description, fingerprint=fingerprint - ) - ), - "firmware_update", - BR_CODE_OTHER, - ) + trezorui2.confirm_firmware_update( + description=description, fingerprint=fingerprint + ), + "firmware_update", + BR_CODE_OTHER, ) diff --git a/core/src/trezor/ui/layouts/tr/fido.py b/core/src/trezor/ui/layouts/tr/fido.py index b05e956db..7fd6c9786 100644 --- a/core/src/trezor/ui/layouts/tr/fido.py +++ b/core/src/trezor/ui/layouts/tr/fido.py @@ -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 diff --git a/core/src/trezor/ui/layouts/tr/homescreen.py b/core/src/trezor/ui/layouts/tr/homescreen.py deleted file mode 100644 index 9bbd74738..000000000 --- a/core/src/trezor/ui/layouts/tr/homescreen.py +++ /dev/null @@ -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 diff --git a/core/src/trezor/ui/layouts/tr/recovery.py b/core/src/trezor/ui/layouts/tr/recovery.py index 41ceae221..030711da5 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -1,17 +1,20 @@ -from typing import Callable, Iterable +from typing import TYPE_CHECKING, Awaitable, Iterable 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 ..common import InfoFunc 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) @@ -19,26 +22,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 @@ -50,22 +58,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, ) @@ -97,7 +103,7 @@ async def continue_recovery( button_label: str, text: str, subtext: str | None, - info_func: Callable | None, + info_func: InfoFunc | None, recovery_type: RecoveryType, show_info: bool = False, ) -> bool: @@ -114,40 +120,29 @@ async def continue_recovery( if subtext: text += f"\n\n{subtext}" - 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, - ) - 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: - pass - else: - return False + homepage = trezorui2.confirm_recovery( + title="", + description=text, + button=button_label.upper(), + 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, + ) + return result is trezorui2.CONFIRMED -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) + return show_warning(br_name, content, subheader, button, br_code) diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index e1754d142..5632a3d76 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -3,13 +3,17 @@ from typing import 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 +if TYPE_CHECKING: + from typing import Awaitable, Sequence + + from trezor.enums import BackupType + async def show_share_words( share_words: Sequence[str], @@ -45,13 +49,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 @@ -82,13 +85,14 @@ async def select_word( while len(words) < 3: words.append(words[-1]) - word_ordinal = format_ordinal(checked_index + 1) - result = await RustLayout( + word_ordinal = format_ordinal(checked_index + 1).upper() + 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 +100,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 +120,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 +139,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 +223,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 +237,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 +251,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 +268,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 +278,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 +289,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, diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index f7ef29c1e..0265ada60 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -1,26 +1,17 @@ from typing import TYPE_CHECKING import trezorui2 -from trezor import TR, io, log, loop, ui, utils +from trezor import TR, ui, utils from trezor.enums import ButtonRequestType -from trezor.messages import ButtonAck, ButtonRequest -from trezor.wire import ActionCancelled, context +from trezor.wire import ActionCancelled -from ..common import button_request, interact +from ..common import interact, raise_if_not_confirmed if TYPE_CHECKING: - from typing import Any, Awaitable, Iterable, NoReturn, Sequence, TypeVar + from typing import Any, Awaitable, Iterable, NoReturn, Sequence from ..common import ExceptionType, PropertyType - T = TypeVar("T") - - LayoutParentType = ui.Layout[T] - -else: - LayoutParentType = [ui.Layout] - T = 0 - BR_CODE_OTHER = ButtonRequestType.Other # global_import_cache @@ -35,240 +26,6 @@ if __debug__: trezorui2.disable_animation(bool(DISABLE_ANIMATION)) -class RustLayout(LayoutParentType[T]): - - # pylint: disable=super-init-not-called - def __init__(self, layout: trezorui2.LayoutObj[T]): - self.br_chan = loop.chan() - self.layout = layout - self.timer = loop.Timer() - self.layout.attach_timer_fn(self.set_timer, None) - self._send_button_request() - self.backlight_level = ui.BacklightLevels.NORMAL - - def __del__(self): - self.layout.__del__() - - def set_timer(self, token: int, deadline: int) -> None: - self.timer.schedule(deadline, token) - - def request_complete_repaint(self) -> None: - msg = self.layout.request_complete_repaint() - assert msg is None - - def _paint(self) -> None: - import storage.cache as storage_cache - - painted = self.layout.paint() - - if painted: - ui.refresh() - if storage_cache.homescreen_shown is not None and painted: - storage_cache.homescreen_shown = None - - if __debug__: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - if context.CURRENT_CONTEXT: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_swipe(), - self.handle_click_signal(), - self.handle_result_signal(), - self.handle_usb(context.get_context()), - ) - else: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_swipe(), - self.handle_click_signal(), - self.handle_result_signal(), - ) - - async def handle_result_signal(self) -> None: - """Enables sending arbitrary input - ui.Result. - - Waits for `result_signal` and carries it out. - """ - from storage import debug as debug_storage - - from apps.debug import result_signal - - while True: - event_id, result = await result_signal() - debug_storage.new_layout_event_id = event_id - raise ui.Result(result) - - def read_content_into(self, content_store: list[str]) -> None: - """Reads all the strings/tokens received from Rust into given list.""" - - def callback(*args: Any) -> None: - for arg in args: - content_store.append(str(arg)) - - content_store.clear() - self.layout.trace(callback) - - async def handle_swipe(self): - from trezor.enums import DebugSwipeDirection - - from apps.debug import notify_layout_change, swipe_signal - - while True: - event_id, direction = await swipe_signal() - 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] - - 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), - ): - msg = self.layout.touch_event(event, x, y) - self._send_button_request() - self._paint() - if msg is not None: - raise ui.Result(msg) - - notify_layout_change(self, event_id) - - async def _click( - self, - event_id: int | None, - x: int, - y: int, - hold_ms: int | None, - ) -> Any: - from storage import debug as debug_storage - from trezor import workflow - - from apps.debug import notify_layout_change - - self.layout.touch_event(io.TOUCH_START, x, y) - self._send_button_request() - self._paint() - if hold_ms is not None: - await loop.sleep(hold_ms) - msg = self.layout.touch_event(io.TOUCH_END, x, y) - self._send_button_request() - - if msg is not None: - debug_storage.new_layout_event_id = event_id - raise ui.Result(msg) - - # So that these presses will keep trezor awake - # (it will not be locked after auto_lock_delay_ms) - workflow.idle_timer.touch() - - self._paint() - notify_layout_change(self, event_id) - - async def handle_click_signal(self) -> None: - """Enables clicking somewhere on the screen. - - Waits for `click_signal` and carries it out. - """ - from apps.debug import click_signal - - while True: - event_id, x, y, hold_ms = await click_signal() - await self._click(event_id, x, y, hold_ms) - - else: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - if context.CURRENT_CONTEXT: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_usb(context.get_context()), - ) - else: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - ) - - def _first_paint(self) -> None: - ui.backlight_fade(ui.BacklightLevels.NONE) - self._paint() - - if __debug__ and self.should_notify_layout_change: - from storage import debug as debug_storage - - from apps.debug import notify_layout_change - - # notify about change and do not notify again until next await. - # (handle_rendering might be called multiple times in a single await, - # because of the endless loop in __iter__) - self.should_notify_layout_change = False - - # Possibly there is an event ID that caused the layout change, - # so notifying with this ID. - event_id = None - if debug_storage.new_layout_event_id is not None: - event_id = debug_storage.new_layout_event_id - debug_storage.new_layout_event_id = None - - notify_layout_change(self, event_id) - - # Turn the brightness on again. - ui.backlight_fade(self.backlight_level) - - def handle_input_and_rendering(self) -> loop.Task: - from trezor import workflow - - touch = loop.wait(io.TOUCH) - self._first_paint() - while True: - # Using `yield` instead of `await` to avoid allocations. - event, x, y = yield touch - workflow.idle_timer.touch() - msg = None - if event in (io.TOUCH_START, io.TOUCH_MOVE, io.TOUCH_END): - msg = self.layout.touch_event(event, x, y) - self._send_button_request() - if msg is not None: - raise ui.Result(msg) - self._paint() - - def handle_timers(self) -> loop.Task: - while True: - # Using `yield` instead of `await` to avoid allocations. - token = yield self.timer - msg = self.layout.timer(token) - self._send_button_request() - if msg is not None: - raise ui.Result(msg) - self._paint() - - def page_count(self) -> int: - return self.layout.page_count() - - async def handle_usb(self, ctx: context.Context): - while True: - br_code, br_name, page_count = await loop.race( - ctx.read(()), self.br_chan.take() - ) - log.debug(__name__, "ButtonRequest.name=%s", br_name) - await ctx.call( - ButtonRequest(code=br_code, pages=page_count, name=br_name), ButtonAck - ) - - def _send_button_request(self): - res = self.layout.button_request() - if res is not None: - br_code, br_name = res - self.br_chan.publish((br_code, br_name, self.layout.page_count())) - - def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None: # Simple drawing not supported for layouts that set timers. def dummy_set_timer(token: int, deadline: int) -> None: @@ -281,14 +38,6 @@ def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None: ui.backlight_fade(ui.BacklightLevels.NORMAL) -async def raise_if_not_confirmed( - a: Awaitable[ui.UiResult], exc: Any = ActionCancelled -) -> None: - result = await a - if result is not CONFIRMED: - raise exc - - def confirm_action( br_name: str, title: str, @@ -310,23 +59,19 @@ def confirm_action( description = description.format(description_param) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_action( - title=title, - action=action, - description=description, - subtitle=subtitle, - verb=verb, - verb_cancel=verb_cancel, - hold=hold, - hold_danger=hold_danger, - reverse=reverse, - ) - ), - br_name, - br_code, + trezorui2.confirm_action( + title=title, + action=action, + description=description, + subtitle=subtitle, + verb=verb, + verb_cancel=verb_cancel, + hold=hold, + hold_danger=hold_danger, + reverse=reverse, ), + br_name, + br_code, exc, ) @@ -342,22 +87,17 @@ def confirm_single( # Placeholders are coming from translations in form of {0} template_str = "{0}" - if template_str not in description: - template_str = "{}" + assert template_str in description begin, _separator, end = description.partition(template_str) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_emphasized( - title=title, - items=(begin, (True, description_param), end), - verb=verb, - ) - ), - br_name, - ButtonRequestType.ProtectCall, - ) + trezorui2.confirm_emphasized( + title=title, + items=(begin, (True, description_param), end), + verb=verb, + ), + br_name, + ButtonRequestType.ProtectCall, ) @@ -368,20 +108,12 @@ def confirm_reset_device(title: str, recovery: bool = False) -> Awaitable[None]: button = TR.reset__button_create return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_reset_device( - title=title, - button=button, - ) - ), - "recover_device" if recovery else "setup_device", - ( - ButtonRequestType.ProtectCall - if recovery - else ButtonRequestType.ResetDevice - ), - ) + trezorui2.confirm_reset_device( + title=title, + button=button, + ), + "recover_device" if recovery else "setup_device", + (ButtonRequestType.ProtectCall if recovery else ButtonRequestType.ResetDevice), ) @@ -393,33 +125,31 @@ async def show_wallet_created_success() -> None: # TODO cleanup @ redesign async def prompt_backup() -> bool: result = await interact( - RustLayout( - trezorui2.confirm_action( - title=TR.words__title_success, - action=TR.backup__new_wallet_successfully_created, - description=TR.backup__it_should_be_backed_up, - verb=TR.buttons__back_up, - verb_cancel=TR.buttons__skip, - ) + trezorui2.confirm_action( + title=TR.words__title_success, + action=TR.backup__new_wallet_successfully_created, + description=TR.backup__it_should_be_backed_up, + verb=TR.buttons__back_up, + verb_cancel=TR.buttons__skip, ), "backup_device", ButtonRequestType.ResetDevice, + raise_on_cancel=None, ) if result is CONFIRMED: return True result = await interact( - RustLayout( - trezorui2.confirm_action( - title=TR.words__warning, - action=TR.backup__want_to_skip, - description=TR.backup__can_back_up_anytime, - verb=TR.buttons__back_up, - verb_cancel=TR.buttons__skip, - ) + trezorui2.confirm_action( + title=TR.words__warning, + action=TR.backup__want_to_skip, + description=TR.backup__can_back_up_anytime, + verb=TR.buttons__back_up, + verb_cancel=TR.buttons__skip, ), "backup_device", ButtonRequestType.ResetDevice, + raise_on_cancel=None, ) return result is CONFIRMED @@ -431,18 +161,14 @@ def confirm_path_warning(path: str, path_type: str | None = None) -> Awaitable[N else f"{TR.words__unknown} {path_type.lower()}." ) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=title, - value=path, - description=TR.words__continue_anyway, - button=TR.buttons__continue, - ) - ), - "path_warning", - br_code=ButtonRequestType.UnknownDerivationPath, - ) + trezorui2.show_warning( + title=title, + value=path, + description=TR.words__continue_anyway, + button=TR.buttons__continue, + ), + "path_warning", + br_code=ButtonRequestType.UnknownDerivationPath, ) @@ -456,16 +182,12 @@ def confirm_multisig_warning() -> Awaitable[None]: def confirm_homescreen(image: bytes) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_homescreen( - title=TR.homescreen__title_set, - image=image, - ) - ), - "set_homesreen", - ButtonRequestType.ProtectCall, - ) + trezorui2.confirm_homescreen( + title=TR.homescreen__title_set, + image=image, + ), + "set_homesreen", + ButtonRequestType.ProtectCall, ) @@ -497,26 +219,21 @@ async def show_address( elif details_title is None: details_title = title - layout = RustLayout( - trezorui2.confirm_address( - title=title, - data=address, - description=network or "", - extra=None, - chunkify=chunkify, - ) - ) - while True: - if send_button_request: - send_button_request = False - await button_request( - br_name, - br_code, - pages=layout.page_count(), - ) - layout.request_complete_repaint() - result = await layout + result = await interact( + trezorui2.confirm_address( + title=title, + data=address, + description=network or "", + extra=None, + chunkify=chunkify, + ), + br_name if send_button_request else None, + br_code, + raise_on_cancel=None, + ) + + send_button_request = False # User pressed right button. if result is CONFIRMED: @@ -534,7 +251,7 @@ async def show_address( ) return result - result = await RustLayout( + result = await interact( trezorui2.show_address_details( qr_title=title, address=address if address_qr is None else address_qr, @@ -543,12 +260,18 @@ async def show_address( account=account, path=path, xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], - ) + ), + None, + raise_on_cancel=None, ) assert result is CANCELLED else: - result = await RustLayout(trezorui2.show_mismatch(title=mismatch_title)) + result = await interact( + trezorui2.show_mismatch(title=mismatch_title), + None, + raise_on_cancel=None, + ) assert result in (CONFIRMED, CANCELLED) # Right button aborts action, left goes back to showing address. if result is CONFIRMED: @@ -587,17 +310,17 @@ async def show_error_and_raise( ) -> NoReturn: button = button or TR.buttons__try_again # def_arg await interact( - RustLayout( - trezorui2.show_error( - title=subheader or "", - description=content, - button=button, - allow_cancel=False, - ) + trezorui2.show_error( + title=subheader or "", + description=content, + button=button, + allow_cancel=False, ), br_name, BR_CODE_OTHER, + raise_on_cancel=None, ) + # always raise regardless of result raise exc @@ -610,17 +333,13 @@ def show_warning( ) -> Awaitable[None]: button = button or TR.buttons__continue # def_arg return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=content, - description=subheader or "", - button=button, - ) - ), - br_name, - br_code, - ) + trezorui2.show_warning( + title=content, + description=subheader or "", + button=button, + ), + br_name, + br_code, ) @@ -632,18 +351,14 @@ def show_success( ) -> Awaitable[None]: button = button or TR.buttons__continue # def_arg return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_success( - title=content, - description=subheader or "", - button=button, - allow_cancel=False, - ) - ), - br_name, - ButtonRequestType.Success, - ) + trezorui2.show_success( + title=content, + description=subheader or "", + button=button, + allow_cancel=False, + ), + br_name, + ButtonRequestType.Success, ) @@ -673,27 +388,24 @@ async def confirm_output( recipient_title = TR.send__title_sending_to while True: - result = await interact( - RustLayout( - trezorui2.confirm_value( - title=recipient_title, - subtitle=address_label, - description=None, - value=address, - verb=TR.buttons__continue, - hold=False, - info_button=False, - chunkify=chunkify, - ) + # if the user cancels here, raise ActionCancelled (by default) + await interact( + trezorui2.confirm_value( + title=recipient_title, + subtitle=address_label, + description=None, + value=address, + verb=TR.buttons__continue, + hold=False, + info_button=False, + chunkify=chunkify, ), "confirm_output", br_code, ) - if result is not CONFIRMED: - raise ActionCancelled - result = await interact( - RustLayout( + try: + await interact( trezorui2.confirm_value( title=amount_title, subtitle=None, @@ -703,12 +415,14 @@ async def confirm_output( verb_cancel="^", hold=hold, info_button=False, - ) - ), - "confirm_output", - br_code, - ) - if result is CONFIRMED: + ), + "confirm_output", + br_code, + ) + except ActionCancelled: + # if the user cancels here, go back to confirm_value + continue + else: return @@ -723,14 +437,12 @@ async def should_show_payment_request_details( Raises ActionCancelled if the user cancels. """ result = await interact( - RustLayout( - trezorui2.confirm_with_info( - title=TR.send__title_sending, - items=[(ui.NORMAL, f"{amount} to\n{recipient_name}")] - + [(ui.NORMAL, memo) for memo in memos], - button=TR.buttons__confirm, - info_button=TR.buttons__details, - ) + trezorui2.confirm_with_info( + title=TR.send__title_sending, + items=[(ui.NORMAL, f"{amount} to\n{recipient_name}")] + + [(ui.NORMAL, memo) for memo in memos], + button=TR.buttons__confirm, + info_button=TR.buttons__details, ), "confirm_payment_request", ButtonRequestType.ConfirmOutput, @@ -762,13 +474,11 @@ async def should_show_more( confirm = TR.buttons__confirm result = await interact( - RustLayout( - trezorui2.confirm_with_info( - title=title, - items=para, - button=confirm, - info_button=button_text, - ) + trezorui2.confirm_with_info( + title=title, + items=para, + button=confirm, + info_button=button_text, ), br_name, br_code, @@ -779,7 +489,6 @@ async def should_show_more( elif result is INFO: return True else: - assert result is CANCELLED raise ActionCancelled @@ -790,12 +499,17 @@ async def _confirm_ask_pagination( description: str, br_code: ButtonRequestType, ) -> None: - paginated: ui.Layout | None = None # TODO: make should_show_more/confirm_more accept bytes directly - if isinstance(data, bytes): + if isinstance(data, (bytes, bytearray, memoryview)): from ubinascii import hexlify data = hexlify(data).decode() + + confirm_more_layout = trezorui2.confirm_more( + title=title, + button=TR.buttons__close, + items=[(ui.MONO, data)], + ) while True: if not await should_show_more( title, @@ -805,19 +519,7 @@ async def _confirm_ask_pagination( ): return - if paginated is None: - paginated = RustLayout( - trezorui2.confirm_more( - title=title, - button=TR.buttons__close, - items=[(ui.MONO, data)], - ) - ) - else: - paginated.request_complete_repaint() - - result = await interact(paginated, br_name, br_code) - assert result in (CONFIRMED, CANCELLED) + await interact(confirm_more_layout, br_name, br_code, raise_on_cancel=None) assert False @@ -836,17 +538,16 @@ def confirm_blob( prompt_screen: bool = True, ) -> Awaitable[None]: verb = verb or TR.buttons__confirm # def_arg - layout = RustLayout( - trezorui2.confirm_blob( - title=title, - description=description, - data=data, - extra=None, - hold=hold, - verb=verb, - verb_cancel=verb_cancel, - chunkify=chunkify, - ) + description = description or "" + layout = trezorui2.confirm_blob( + title=title, + description=description, + data=data, + extra=None, + hold=hold, + verb=verb, + verb_cancel=verb_cancel, + chunkify=chunkify, ) if ask_pagination and layout.page_count() > 1: @@ -854,13 +555,7 @@ def confirm_blob( return _confirm_ask_pagination(br_name, title, data, description or "", br_code) else: - return raise_if_not_confirmed( - interact( - layout, - br_name, - br_code, - ) - ) + return raise_if_not_confirmed(layout, br_name, br_code) def confirm_address( @@ -936,32 +631,26 @@ def confirm_value( raise ValueError("Either verb or hold=True must be set") info_items = info_items or [] - info_layout = RustLayout( - trezorui2.show_info_with_cancel( - title=info_title if info_title else TR.words__title_information, - items=info_items, - chunkify=chunkify_info, - ) + info_layout = trezorui2.show_info_with_cancel( + title=info_title if info_title else TR.words__title_information, + items=info_items, + chunkify=chunkify_info, ) - return raise_if_not_confirmed( - with_info( - RustLayout( - trezorui2.confirm_value( - title=title, - subtitle=subtitle, - description=description, - value=value, - verb=verb, - hold=hold, - info_button=bool(info_items), - text_mono=value_text_mono, - ) - ), - info_layout, - br_name, - br_code, - ) + return with_info( + trezorui2.confirm_value( + title=title, + subtitle=subtitle, + description=description, + value=value, + verb=verb, + hold=hold, + info_button=bool(info_items), + text_mono=value_text_mono, + ), + info_layout, + br_name, + br_code, ) @@ -976,17 +665,13 @@ def confirm_properties( items = [(prop[0], prop[1], isinstance(prop[1], bytes)) for prop in props] return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_properties( - title=title, - items=items, - hold=hold, - ) - ), - br_name, - br_code, - ) + trezorui2.confirm_properties( + title=title, + items=items, + hold=hold, + ), + br_name, + br_code, ) @@ -1035,23 +720,17 @@ def confirm_summary( ) -> Awaitable[None]: title = title or TR.words__title_summary # def_arg - total_layout = RustLayout( - trezorui2.confirm_total( - title=title, - items=items, - info_button=bool(info_items), - ) + total_layout = trezorui2.confirm_total( + title=title, + items=items, + info_button=bool(info_items), ) info_items = info_items or [] - info_layout = RustLayout( - trezorui2.show_info_with_cancel( - title=info_title if info_title else TR.words__title_information, - items=info_items, - ) - ) - return raise_if_not_confirmed( - with_info(total_layout, info_layout, br_name, br_code) + info_layout = trezorui2.show_info_with_cancel( + title=info_title if info_title else TR.words__title_information, + items=info_items, ) + return with_info(total_layout, info_layout, br_name, br_code) if not utils.BITCOIN_ONLY: @@ -1065,22 +744,18 @@ if not utils.BITCOIN_ONLY: br_code: ButtonRequestType = ButtonRequestType.SignTx, chunkify: bool = False, ) -> None: - total_layout = RustLayout( - trezorui2.confirm_total( - title=TR.words__title_summary, - items=[ - (f"{TR.words__amount}:", total_amount), - (TR.send__maximum_fee, maximum_fee), - ], - info_button=True, - cancel_arrow=True, - ) + total_layout = trezorui2.confirm_total( + title=TR.words__title_summary, + items=[ + (f"{TR.words__amount}:", total_amount), + (TR.send__maximum_fee, maximum_fee), + ], + info_button=True, + cancel_arrow=True, ) - info_layout = RustLayout( - trezorui2.show_info_with_cancel( - title=TR.confirm_total__title_fee, - items=items, - ) + info_layout = trezorui2.show_info_with_cancel( + title=TR.confirm_total__title_fee, + items=items, ) while True: @@ -1094,13 +769,11 @@ if not utils.BITCOIN_ONLY: ) try: - total_layout.request_complete_repaint() - await raise_if_not_confirmed( - with_info(total_layout, info_layout, br_name, br_code) - ) - break + await with_info(total_layout, info_layout, br_name, br_code) except ActionCancelled: continue + else: + break async def confirm_ethereum_staking_tx( title: str, @@ -1169,19 +842,15 @@ if not utils.BITCOIN_ONLY: def confirm_joint_total(spending_amount: str, total_amount: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_total( - title=TR.send__title_joint_transaction, - items=[ - (TR.send__you_are_contributing, spending_amount), - (TR.send__to_the_total_amount, total_amount), - ], - ) - ), - "confirm_joint_total", - ButtonRequestType.SignTx, - ) + trezorui2.confirm_total( + title=TR.send__title_joint_transaction, + items=[ + (TR.send__you_are_contributing, spending_amount), + (TR.send__to_the_total_amount, total_amount), + ], + ), + "confirm_joint_total", + ButtonRequestType.SignTx, ) @@ -1224,68 +893,62 @@ async def confirm_modify_output( amount_change: str, amount_new: str, ) -> None: - address_layout = RustLayout( - trezorui2.confirm_blob( - title=TR.modify_amount__title, - data=address, - verb=TR.buttons__continue, - verb_cancel=None, - description=f"{TR.words__address}:", - extra=None, - ) - ) - modify_layout = RustLayout( - trezorui2.confirm_modify_output( - sign=sign, - amount_change=amount_change, - amount_new=amount_new, - ) - ) - send_button_request = True while True: - if send_button_request: - await button_request( - "modify_output", - ButtonRequestType.ConfirmOutput, - address_layout.page_count(), - ) - address_layout.request_complete_repaint() - await raise_if_not_confirmed(address_layout) + # if the user cancels here, raise ActionCancelled (by default) + await interact( + trezorui2.confirm_blob( + title="MODIFY AMOUNT", + data=address, + verb="CONTINUE", + verb_cancel=None, + description="Address:", + extra=None, + ), + "modify_output" if send_button_request else None, + ButtonRequestType.ConfirmOutput, + ) - if send_button_request: + try: + await interact( + trezorui2.confirm_modify_output( + sign=sign, + amount_change=amount_change, + amount_new=amount_new, + ), + "modify_output" if send_button_request else None, + ButtonRequestType.ConfirmOutput, + ) + except ActionCancelled: + # if the user cancels here, go back to confirm_blob send_button_request = False - await button_request( - "modify_output", - ButtonRequestType.ConfirmOutput, - modify_layout.page_count(), - ) - modify_layout.request_complete_repaint() - result = await modify_layout - - if result is CONFIRMED: - break + continue + else: + return async def with_info( - main_layout: RustLayout[T], - info_layout: RustLayout[Any], + main_layout: ui.LayoutObj[ui.UiResult], + info_layout: ui.LayoutObj[Any], br_name: str, br_code: ButtonRequestType, -) -> T: - await button_request(br_name, br_code, pages=main_layout.page_count()) +) -> None: + send_button_request = True while True: - result = await main_layout + 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 INFO: - info_layout.request_complete_repaint() - result = await info_layout - assert result is CANCELLED - main_layout.request_complete_repaint() + if result is CONFIRMED: + return + elif result is INFO: + await interact(info_layout, None, raise_on_cancel=None) continue else: - return result + raise RuntimeError # unexpected result def confirm_modify_fee( @@ -1295,41 +958,31 @@ def confirm_modify_fee( total_fee_new: str, fee_rate_amount: str | None = None, ) -> Awaitable[None]: - fee_layout = RustLayout( - trezorui2.confirm_modify_fee( - title=title, - sign=sign, - user_fee_change=user_fee_change, - total_fee_new=total_fee_new, - fee_rate_amount=fee_rate_amount, - ) + fee_layout = trezorui2.confirm_modify_fee( + title=title, + sign=sign, + user_fee_change=user_fee_change, + total_fee_new=total_fee_new, + fee_rate_amount=fee_rate_amount, ) items: list[tuple[str, str]] = [] if fee_rate_amount: items.append((TR.bitcoin__new_fee_rate, fee_rate_amount)) - info_layout = RustLayout( - trezorui2.show_info_with_cancel( - title=TR.confirm_total__title_fee, - items=items, - ) - ) - return raise_if_not_confirmed( - with_info(fee_layout, info_layout, "modify_fee", ButtonRequestType.SignTx) + info_layout = trezorui2.show_info_with_cancel( + title=TR.confirm_total__title_fee, + items=items, ) + return with_info(fee_layout, info_layout, "modify_fee", ButtonRequestType.SignTx) def confirm_coinjoin(max_rounds: int, max_fee_per_vbyte: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_coinjoin( - max_rounds=str(max_rounds), - max_feerate=max_fee_per_vbyte, - ) - ), - "coinjoin_final", - BR_CODE_OTHER, - ) + trezorui2.confirm_coinjoin( + max_rounds=str(max_rounds), + max_feerate=max_fee_per_vbyte, + ), + "coinjoin_final", + BR_CODE_OTHER, ) @@ -1361,15 +1014,13 @@ async def confirm_signverify( address_title = TR.sign_message__confirm_address br_name = "sign_message" - address_layout = RustLayout( - trezorui2.confirm_address( - title=address_title, - data=address, - description="", - verb=TR.buttons__continue, - extra=None, - chunkify=chunkify, - ) + address_layout = trezorui2.confirm_address( + title=address_title, + data=address, + description="", + verb=TR.buttons__continue, + extra=None, + chunkify=chunkify, ) items: list[tuple[str, str]] = [] @@ -1384,50 +1035,45 @@ async def confirm_signverify( ) ) - info_layout = RustLayout( - trezorui2.show_info_with_cancel( - title=TR.words__title_information, - items=items, - horizontal=True, - ) + info_layout = trezorui2.show_info_with_cancel( + title=TR.words__title_information, + items=items, + horizontal=True, ) - message_layout = RustLayout( - trezorui2.confirm_blob( - title=TR.sign_message__confirm_message, - description=None, - data=message, - extra=None, - hold=not verify, - verb=TR.buttons__confirm if verify else None, - ) + message_layout = trezorui2.confirm_blob( + title=TR.sign_message__confirm_message, + description=None, + data=message, + extra=None, + hold=not verify, + verb=TR.buttons__confirm if verify else None, ) while True: - result = await with_info( - address_layout, info_layout, br_name, br_code=BR_CODE_OTHER - ) - if result is not CONFIRMED: - result = await RustLayout( - trezorui2.show_mismatch(title=TR.addr_mismatch__mismatch) + try: + await with_info(address_layout, info_layout, br_name, br_code=BR_CODE_OTHER) + except ActionCancelled: + result = await interact( + trezorui2.show_mismatch(title=TR.addr_mismatch__mismatch), + None, + raise_on_cancel=None, ) assert result in (CONFIRMED, CANCELLED) # Right button aborts action, left goes back to showing address. if result is CONFIRMED: raise ActionCancelled else: - address_layout.request_complete_repaint() continue - message_layout.request_complete_repaint() - result = await interact(message_layout, br_name, BR_CODE_OTHER) + result = await interact( + message_layout, br_name, BR_CODE_OTHER, raise_on_cancel=None + ) if result is CONFIRMED: break - address_layout.request_complete_repaint() - -def show_error_popup( +def error_popup( title: str, description: str, subtitle: str | None = None, @@ -1435,22 +1081,20 @@ def show_error_popup( *, button: str = "", timeout_ms: int = 0, -) -> Awaitable[None]: +) -> ui.LayoutObj[None]: if not button and not timeout_ms: raise ValueError("Either button or timeout_ms must be set") if subtitle: title += f"\n{subtitle}" - layout = RustLayout( - trezorui2.show_error( - title=title, - description=description.format(description_param), - button=button, - time_ms=timeout_ms, - allow_cancel=False, - ) + layout = trezorui2.show_error( + title=title, + description=description.format(description_param), + button=button, + time_ms=timeout_ms, + allow_cancel=False, ) - return layout # type: ignore [Expression of type "RustLayout[UiResult]" is incompatible with return type "Awaitable[None]"] + return layout # type: ignore [Expression of type "LayoutObj[UiResult]" is incompatible with return type "LayoutObj[None]"] def request_passphrase_on_host() -> None: @@ -1468,17 +1112,11 @@ def show_wait_text(message: str) -> None: async def request_passphrase_on_device(max_len: int) -> str: result = await interact( - RustLayout( - trezorui2.request_passphrase( - prompt=TR.passphrase__title_enter, max_len=max_len - ) - ), + trezorui2.request_passphrase(prompt="Enter passphrase", max_len=max_len), "passphrase_device", ButtonRequestType.PassphraseEntry, + raise_on_cancel=ActionCancelled("Passphrase entry cancelled"), ) - if result is CANCELLED: - raise ActionCancelled("Passphrase entry cancelled") - assert isinstance(result, str) return result @@ -1499,19 +1137,16 @@ async def request_pin_on_device( subprompt = f"{attempts_remaining} {TR.pin__tries_left}" result = await interact( - RustLayout( - trezorui2.request_pin( - prompt=prompt, - subprompt=subprompt, - allow_cancel=allow_cancel, - wrong_pin=wrong_pin, - ) + trezorui2.request_pin( + prompt=prompt, + subprompt=subprompt, + allow_cancel=allow_cancel, + wrong_pin=wrong_pin, ), "pin_device", ButtonRequestType.PinEntry, + raise_on_cancel=PinCancelled, ) - if result is CANCELLED: - raise PinCancelled assert isinstance(result, str) return result @@ -1521,23 +1156,31 @@ async def confirm_reenter_pin(is_wipe_code: bool = False) -> None: pass -async def pin_mismatch_popup(is_wipe_code: bool = False) -> None: - await button_request("pin_mismatch", code=BR_CODE_OTHER) +def pin_mismatch_popup(is_wipe_code: bool = False) -> Awaitable[None]: title = TR.wipe_code__wipe_code_mismatch if is_wipe_code else TR.pin__pin_mismatch description = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch - return await show_error_popup( - title, - description, - button=TR.buttons__try_again, + return interact( + error_popup( + title, + description, + button=TR.buttons__try_again, + ), + "pin_mismatch", + BR_CODE_OTHER, + raise_on_cancel=None, ) -async def wipe_code_same_as_pin_popup() -> None: - await button_request("wipe_code_same_as_pin", code=BR_CODE_OTHER) - return await show_error_popup( - TR.wipe_code__invalid, - TR.wipe_code__diff_from_pin, - button=TR.buttons__try_again, +def wipe_code_same_as_pin_popup() -> Awaitable[None]: + return interact( + error_popup( + TR.wipe_code__invalid, + TR.wipe_code__diff_from_pin, + button=TR.buttons__try_again, + ), + "wipe_code_same_as_pin", + BR_CODE_OTHER, + raise_on_cancel=None, ) @@ -1549,40 +1192,32 @@ def confirm_set_new_pin( br_code: ButtonRequestType = BR_CODE_OTHER, ) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_emphasized( - title=title, - items=( - (True, description + "\n\n"), - information, - ), - verb=TR.buttons__turn_on, - ) + trezorui2.confirm_emphasized( + title=title, + items=( + (True, description + "\n\n"), + information, ), - br_name, - br_code, - ) + verb=TR.buttons__turn_on, + ), + br_name, + br_code, ) def confirm_firmware_update(description: str, fingerprint: str) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.confirm_firmware_update( - description=description, fingerprint=fingerprint - ) - ), - "firmware_update", - BR_CODE_OTHER, - ) + trezorui2.confirm_firmware_update( + description=description, fingerprint=fingerprint + ), + "firmware_update", + BR_CODE_OTHER, ) async def set_brightness(current: int | None = None) -> None: await interact( - RustLayout(trezorui2.set_brightness(current=current)), + trezorui2.set_brightness(current=current), "set_brightness", BR_CODE_OTHER, ) diff --git a/core/src/trezor/ui/layouts/tt/fido.py b/core/src/trezor/ui/layouts/tt/fido.py index 622e19958..be1177d51 100644 --- a/core/src/trezor/ui/layouts/tt/fido.py +++ b/core/src/trezor/ui/layouts/tt/fido.py @@ -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,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 + confirm.paint() + ui.refresh() + msg = confirm.touch_event(io.TOUCH_END, 220, 220) + 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): @@ -78,7 +50,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 +58,4 @@ async def confirm_fido_reset() -> bool: reverse=True, ) ) - return (await confirm) is trezorui2.CONFIRMED + return (await confirm.get_result()) is trezorui2.CONFIRMED diff --git a/core/src/trezor/ui/layouts/tt/homescreen.py b/core/src/trezor/ui/layouts/tt/homescreen.py deleted file mode 100644 index 7089e8e5f..000000000 --- a/core/src/trezor/ui/layouts/tt/homescreen.py +++ /dev/null @@ -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 diff --git a/core/src/trezor/ui/layouts/tt/recovery.py b/core/src/trezor/ui/layouts/tt/recovery.py index 9163987c8..979f319ae 100644 --- a/core/src/trezor/ui/layouts/tt/recovery.py +++ b/core/src/trezor/ui/layouts/tt/recovery.py @@ -1,63 +1,58 @@ -from typing import Callable, Iterable +from typing import TYPE_CHECKING, Awaitable, Iterable 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 trezor.enums import RecoveryType - -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 + from ..common import InfoFunc 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: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words shares_remaining: list[int], group_threshold: int, -) -> None: +) -> Awaitable[trezorui2.UiResult]: from trezor import strings from trezor.crypto.slip39 import MAX_SHARE_COUNT @@ -83,31 +78,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, ) @@ -139,67 +130,56 @@ async def continue_recovery( button_label: str, text: str, subtext: str | None, - info_func: Callable | None, + info_func: InfoFunc | None, recovery_type: RecoveryType, show_info: bool = False, ) -> bool: - from trezor.wire import ActionCancelled - - from ..common import button_request - if show_info: # 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=info_func is not None, + ) + + send_button_request = True while True: - homepage = RustLayout( - trezorui2.confirm_recovery( - title=text, - description=description, - button=button_label, - recovery_type=recovery_type, - info_button=info_func is not None, - ) + result = await interact( + homepage, + "recovery" if send_button_request else None, + ButtonRequestType.RecoveryHomepage, + raise_on_cancel=None, ) + send_button_request = False - await button_request("recovery", ButtonRequestType.RecoveryHomepage) - - result = ( - await homepage - if info_func is None - else await _homepage_with_info(homepage, info_func) - ) - if result is CONFIRMED: - return True - try: - await _confirm_abort(recovery_type != RecoveryType.NormalRecovery) - except ActionCancelled: - pass + if info_func is not None and result is trezorui2.INFO: + await info_func() else: - return False + return result is trezorui2.CONFIRMED -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, ) diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index f887d5894..650ae9265 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -1,12 +1,11 @@ -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 +34,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 +50,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 +83,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 +99,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 +119,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 +140,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 +153,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 +222,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 +233,7 @@ async def slip39_prompt_threshold( ) -async def slip39_prompt_number_of_shares( - num_words: int, group_id: int | None = None -) -> int: +def slip39_prompt_number_of_shares(group_id: int | None = None) -> Awaitable[int]: count = 5 min_count = 1 max_count = 16 @@ -266,7 +256,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 +267,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 +285,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 +303,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 +344,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.upper(), + allow_cancel=False, + ), + br_name, + br_code, ) @@ -387,6 +369,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 diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 5786e1590..b7427e300 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -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 IntEnum @@ -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__) @@ -372,6 +383,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 @@ -386,7 +420,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 @@ -394,6 +427,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.""" @@ -415,7 +453,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}, @@ -426,13 +469,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) @@ -448,11 +490,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 @@ -463,11 +514,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: @@ -511,56 +589,102 @@ 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 current wait mode is used: + + - when in normal tests, IMMEDIATE, 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.) + - when in tests running through a `DeviceHandler`, CURRENT_LAYOUT, which waits + for the next layout to come up. 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: @@ -579,127 +703,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)) @@ -733,44 +738,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" ) @@ -778,6 +774,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 @@ -835,15 +834,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") @@ -855,7 +848,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") @@ -866,7 +859,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 diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 230fc4c5b..2b5022e93 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -552,6 +552,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 @@ -4028,7 +4034,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__( @@ -4036,7 +4042,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 diff --git a/python/src/trezorlib/protobuf.py b/python/src/trezorlib/protobuf.py index 836716a54..e590e216f 100644 --- a/python/src/trezorlib/protobuf.py +++ b/python/src/trezorlib/protobuf.py @@ -510,12 +510,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): diff --git a/rust/trezor-client/src/protos/generated/messages_debug.rs b/rust/trezor-client/src/protos/generated/messages_debug.rs index e69ec5ec5..3ced865e2 100644 --- a/rust/trezor-client/src/protos/generated/messages_debug.rs +++ b/rust/trezor-client/src/protos/generated/messages_debug.rs @@ -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, // @@protoc_insertion_point(field:hw.trezor.messages.debug.DebugLinkGetState.wait_layout) - pub wait_layout: ::std::option::Option, + pub wait_layout: ::std::option::Option<::protobuf::EnumOrUnknown>, // 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; } +/// 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 { + 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 { + 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::("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\x0emessag\ es.proto\x1a\x15messages-common.proto\x1a\x19messages-management.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, diff --git a/tests/device_handler.py b/tests/device_handler.py index 0ea232f5b..364d6351d 100644 --- a/tests/device_handler.py +++ b/tests/device_handler.py @@ -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: diff --git a/tests/device_tests/bitcoin/test_signmessage.py b/tests/device_tests/bitcoin/test_signmessage.py index 0a5e8a796..80b36660c 100644 --- a/tests/device_tests/bitcoin/test_signmessage.py +++ b/tests/device_tests/bitcoin/test_signmessage.py @@ -392,8 +392,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, ] )