diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index bdac48b0c2..0edfd4979f 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/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index e4c2e94083..9f131813b9 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -1634,7 +1634,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Attach a timer setter function. /// /// The layout object can call the timer setter with two arguments, - /// `token` and `duration`. When `duration` elapses, the layout object + /// `token` and `duration_ms`. When `duration_ms` elapses, the layout object /// expects a callback to `self.timer(token)`. /// """ /// diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 3ccc81adc0..33675ae16b 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -1075,10 +1075,10 @@ class LayoutObj(Generic[T]): """Representation of a Rust-based layout object. see `trezor::ui::layout::obj::LayoutObj`. """ - def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> None: + def attach_timer_fn(self, fn: Callable[[int, int], None], attach_type: AttachType | None) -> LayoutState | None: """Attach a timer setter function. The layout object can call the timer setter with two arguments, - `token` and `duration`. When `duration` elapses, the layout object + `token` and `duration_ms`. When `duration_ms` elapses, the layout object expects a callback to `self.timer(token)`. """ if utils.USE_TOUCH: diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 386a6a3c91..c24472d5fc 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 @@ -167,8 +169,6 @@ trezor.ui.layouts.mercury import trezor.ui.layouts.mercury trezor.ui.layouts.mercury.fido import trezor.ui.layouts.mercury.fido -trezor.ui.layouts.mercury.homescreen -import trezor.ui.layouts.mercury.homescreen trezor.ui.layouts.mercury.recovery import trezor.ui.layouts.mercury.recovery trezor.ui.layouts.mercury.reset @@ -183,8 +183,6 @@ trezor.ui.layouts.tr import trezor.ui.layouts.tr trezor.ui.layouts.tr.fido import trezor.ui.layouts.tr.fido -trezor.ui.layouts.tr.homescreen -import trezor.ui.layouts.tr.homescreen trezor.ui.layouts.tr.recovery import trezor.ui.layouts.tr.recovery trezor.ui.layouts.tr.reset @@ -193,8 +191,6 @@ trezor.ui.layouts.tt import trezor.ui.layouts.tt trezor.ui.layouts.tt.fido import trezor.ui.layouts.tt.fido -trezor.ui.layouts.tt.homescreen -import trezor.ui.layouts.tt.homescreen trezor.ui.layouts.tt.recovery import trezor.ui.layouts.tt.recovery trezor.ui.layouts.tt.reset diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py index 8f7e6ec462..8f9fed1e51 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 c375240c2c..f435afddeb 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 e9d9d0ad0c..07f669237b 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/common/sdcard.py b/core/src/apps/common/sdcard.py index 440eeda9f8..3713030226 100644 --- a/core/src/apps/common/sdcard.py +++ b/core/src/apps/common/sdcard.py @@ -1,12 +1,7 @@ -from typing import TYPE_CHECKING - from trezor import TR, io, utils, wire from trezor.ui.layouts import confirm_action, show_error_and_raise from trezor.utils import sd_hotswap_enabled -if TYPE_CHECKING: - from trezor.ui.layouts.common import ProgressLayout - class SdCardUnavailable(wire.ProcessError): pass @@ -102,6 +97,7 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None: mounted. """ from trezor import sdcard + from trezor.ui.layouts.progress import progress while not sdcard.is_present(): await _confirm_retry_insert_card() @@ -125,11 +121,12 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None: # Proceed to formatting. Failure is caught by the outside OSError handler with sdcard.filesystem(mounted=False): - _start_progress() - fatfs.mkfs(_render_progress) + progress_obj = progress() + progress_obj.start() + fatfs.mkfs(progress_obj.report) fatfs.mount() fatfs.setlabel("TREZOR") - _finish_progress() + progress_obj.stop() # format and mount succeeded return @@ -157,30 +154,3 @@ async def request_sd_salt() -> bytearray | None: # In either case, there is no good way to recover. If the user clicks Retry, # we will try again. await confirm_retry_sd() - - -_progress_obj: ProgressLayout | None = None - - -def _start_progress() -> None: - from trezor import workflow - from trezor.ui.layouts.progress import progress - - global _progress_obj - - if not utils.DISABLE_ANIMATION: - # Because we are drawing to the screen manually, without a layout, we - # should make sure that no other layout is running. - workflow.close_others() - _progress_obj = progress() - - -def _render_progress(progress: int) -> None: - global _progress_obj - if _progress_obj is not None: - _progress_obj.report(progress) - - -def _finish_progress() -> None: - global _progress_obj - _progress_obj = None diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index c9d81389b5..94dd3bb783 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -4,20 +4,21 @@ if not __debug__: halt("debug mode inactive") if __debug__: + import utime + from micropython import const from typing import TYPE_CHECKING import trezorui2 from storage import debug as storage - from storage.debug import debug_events - from trezor import log, loop, utils, wire - from trezor.enums import MessageType - from trezor.messages import DebugLinkLayout, Success + from trezor import io, log, loop, ui, utils, wire, workflow + from trezor.enums import DebugWaitType, MessageType + from trezor.messages import Success from trezor.ui import display - from trezor.wire import context - - from apps import workflow_handlers if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + + from trezor.enums import DebugButton, DebugPhysicalButton, DebugSwipeDirection from trezor.messages import ( DebugLinkDecision, DebugLinkEraseSdCard, @@ -25,33 +26,22 @@ if __debug__: DebugLinkOptigaSetSecMax, DebugLinkRecordScreen, DebugLinkReseedRandom, - DebugLinkResetDebugEvents, DebugLinkState, - DebugLinkWatchLayout, ) from trezor.ui import Layout + from trezor.wire import WireInterface, context - swipe_chan = loop.chan() - result_chan = loop.chan() - button_chan = loop.chan() - click_chan = loop.chan() - swipe_signal = swipe_chan.take - result_signal = result_chan.take - button_signal = button_chan.take - click_signal = click_chan.take + Handler = Callable[[Any], Awaitable[Any]] - debuglink_decision_chan = loop.chan() - - layout_change_chan = loop.chan() + layout_change_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_SLEEP_MS = const(3000) + _DEADLOCK_DETECT_SLEEP = loop.sleep(_DEADLOCK_SLEEP_MS) + def screenshot() -> bool: if storage.save_screen: # Starting with "refresh00", allowing for 100 emulator restarts @@ -62,173 +52,244 @@ 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 layout_is_ready() -> bool: + layout = ui.CURRENT_LAYOUT + return isinstance(layout, ui.Layout) and layout.is_layout_attached() + + def wait_until_layout_is_running(timeout: int | None = _DEADLOCK_SLEEP_MS) -> Awaitable[None]: # type: ignore [awaitable-return-type] + start = utime.ticks_ms() + layout_change_chan.clear() + while not layout_is_ready(): + yield layout_change_chan # type: ignore [awaitable-return-type] + now = utime.ticks_ms() + if timeout and utime.ticks_diff(now, start) > timeout: + raise wire.FirmwareError( + "layout deadlock detected (did you send a ButtonAck?)" + ) + + async def return_layout_change( + ctx: wire.context.Context, detect_deadlock: bool = False ) -> None: + # set up the wait + storage.layout_watcher = True + + # wait for layout change + while True: + if not detect_deadlock or not layout_change_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 isinstance(next_layout, int): + # sleep result from the deadlock detector + raise wire.FirmwareError("layout deadlock detected") + + if next_layout is None: + # we are reading the "layout ended" event, spin once more to grab the + # "new layout started" event + continue + + if layout_is_ready(): + break + + assert ui.CURRENT_LAYOUT is next_layout + + # send the message and reset the wait + storage.layout_watcher = False + await ctx.write(_state()) + + async def _layout_click(x: int, y: int, hold_ms: int = 0) -> None: + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + ui.CURRENT_LAYOUT._event( + ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_START, x, y + ) + + if hold_ms: + await loop.sleep(hold_ms) + workflow.idle_timer.touch() + + if not layout_is_ready(): + return + ui.CURRENT_LAYOUT._event( + ui.CURRENT_LAYOUT.layout.touch_event, io.TOUCH_END, x, y + ) + + async def _layout_press_button( + debug_btn: DebugPhysicalButton, hold_ms: int = 0 + ) -> None: + from trezor.enums import DebugPhysicalButton + + buttons = [] + + if debug_btn == DebugPhysicalButton.LEFT_BTN: + buttons.append(io.BUTTON_LEFT) + elif debug_btn == DebugPhysicalButton.RIGHT_BTN: + buttons.append(io.BUTTON_RIGHT) + elif debug_btn == DebugPhysicalButton.MIDDLE_BTN: + buttons.append(io.BUTTON_LEFT) + buttons.append(io.BUTTON_RIGHT) + + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + for btn in buttons: + ui.CURRENT_LAYOUT._event( + ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_PRESSED, btn + ) + + if hold_ms: + await loop.sleep(hold_ms) + workflow.idle_timer.touch() + + if not layout_is_ready(): + return + for btn in buttons: + ui.CURRENT_LAYOUT._event( + ui.CURRENT_LAYOUT.layout.button_event, io.BUTTON_RELEASED, btn + ) + + if utils.USE_TOUCH: + + async def _layout_swipe(direction: DebugSwipeDirection) -> None: # type: ignore [obscured by a declaration of the same name] + from trezor.enums import DebugSwipeDirection + + orig_x = orig_y = 120 + off_x, off_y = { + DebugSwipeDirection.UP: (0, -30), + DebugSwipeDirection.DOWN: (0, 30), + DebugSwipeDirection.LEFT: (-30, 0), + DebugSwipeDirection.RIGHT: (30, 0), + }[direction] + + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + for event, x, y in ( + (io.TOUCH_START, orig_x, orig_y), + (io.TOUCH_MOVE, orig_x + 1 * off_x, orig_y + 1 * off_y), + (io.TOUCH_END, orig_x + 2 * off_x, orig_y + 2 * off_y), + ): + ui.CURRENT_LAYOUT._event( + ui.CURRENT_LAYOUT.layout.touch_event, event, x, y + ) + + elif utils.USE_BUTTON: + + def _layout_swipe(direction: DebugSwipeDirection) -> Awaitable[None]: + from trezor.enums import DebugPhysicalButton, DebugSwipeDirection + + if direction == DebugSwipeDirection.UP: + button = DebugPhysicalButton.RIGHT_BTN + elif direction == DebugSwipeDirection.DOWN: + button = DebugPhysicalButton.LEFT_BTN + else: + raise RuntimeError # unsupported swipe direction on TR + + return _layout_press_button(button) + + else: + raise RuntimeError # No way to swipe with no buttons and no touches + + async def _layout_event(button: DebugButton) -> None: from trezor.enums import DebugButton - if msg.button is not None: - if msg.button == DebugButton.NO: - await result_chan.put((event_id, trezorui2.CANCELLED)) - elif msg.button == DebugButton.YES: - await result_chan.put((event_id, trezorui2.CONFIRMED)) - elif msg.button == DebugButton.INFO: - await result_chan.put((event_id, trezorui2.INFO)) - else: - raise RuntimeError(f"Invalid msg.button - {msg.button}") - elif msg.input is not None: - await result_chan.put((event_id, msg.input)) - elif msg.swipe is not None: - await swipe_chan.put((event_id, msg.swipe)) + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + if button == DebugButton.NO: + ui.CURRENT_LAYOUT._emit_message(trezorui2.CANCELLED) + elif button == DebugButton.YES: + ui.CURRENT_LAYOUT._emit_message(trezorui2.CONFIRMED) + elif button == DebugButton.INFO: + ui.CURRENT_LAYOUT._emit_message(trezorui2.INFO) else: - # Sanity check. The message will be visible in terminal. - raise RuntimeError("Invalid DebugLinkDecision message") + raise RuntimeError("Invalid DebugButton") - async def debuglink_decision_dispatcher() -> None: - while True: - event_id, msg = await debuglink_decision_chan.take() - await _dispatch_debuglink_decision(event_id, msg) - - async def get_layout_change_content() -> list[str]: - awaited_event_id = debug_events.awaited_event - last_result_id = debug_events.last_result - - if awaited_event_id is not None and awaited_event_id == last_result_id: - # We are awaiting the event that just happened - return current state - return storage.current_content_tokens - - while True: - event_id, content = await layout_change_chan.take() - if awaited_event_id is None or event_id is None: - # Not waiting for anything or event does not have ID - break - elif event_id == awaited_event_id: - # We found what we were waiting for - debug_events.awaited_event = None - break - elif event_id > awaited_event_id: - # Sanity check - pass - # TODO: find out why this sometimes happens on TR when running tests with - # "physical" emulator (./emu.py) - # raise RuntimeError( - # f"Waiting for event that already happened - {event_id} > {awaited_event_id}" - # ) - - if awaited_event_id is not None: - # Updating last result - debug_events.last_result = awaited_event_id - - return content - - async def return_layout_change() -> None: # type: ignore [Return type of async generator] - content_tokens = await get_layout_change_content() - - # spin for a bit until DEBUG_CONTEXT becomes available - while not isinstance(DEBUG_CONTEXT, context.Context): - yield # type: ignore [Return type of async generator] - - if storage.layout_watcher is LAYOUT_WATCHER_LAYOUT: - await DEBUG_CONTEXT.write(DebugLinkLayout(tokens=content_tokens)) - else: - from trezor.messages import DebugLinkState - - await DEBUG_CONTEXT.write(DebugLinkState(tokens=content_tokens)) - storage.layout_watcher = LAYOUT_WATCHER_NONE - - async def dispatch_DebugLinkWatchLayout(msg: DebugLinkWatchLayout) -> Success: - from trezor import ui - - layout_change_chan.putters.clear() - if msg.watch: - await ui.wait_until_layout_is_running() - storage.watch_layout_changes = bool(msg.watch) - log.debug(__name__, "Watch layout changes: %s", storage.watch_layout_changes) - return Success() - - async def dispatch_DebugLinkResetDebugEvents( - msg: DebugLinkResetDebugEvents, - ) -> Success: - # Resetting the debug events makes sure that the previous - # events/layouts are not mixed with the new ones. - storage.reset_debug_events() - return Success() - - async def dispatch_DebugLinkDecision(msg: DebugLinkDecision) -> None: - from trezor import workflow + async def dispatch_DebugLinkDecision( + msg: DebugLinkDecision, + ) -> DebugLinkState | None: + from trezor import ui, workflow workflow.idle_timer.touch() - if debuglink_decision_chan.putters: - log.warning(__name__, "DebugLinkDecision queue is not empty") - x = msg.x # local_cache_attribute y = msg.y # local_cache_attribute - # Incrementing the counter for last events so we know what to await - debug_events.last_event += 1 + await wait_until_layout_is_running() + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + layout_change_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 # pylint: disable=internal-model-tuple-comparison - in ("T2T1", "T3T1", "D001") - ): - click_chan.publish((debug_events.last_event, x, y, msg.hold_ms)) - # Button devices press specific button - elif ( - msg.physical_button is not None - and utils.INTERNAL_MODEL # pylint: disable=internal-model-tuple-comparison - in ("T2B1", "T3B1") - ): - button_chan.publish( - (debug_events.last_event, msg.physical_button, msg.hold_ms) - ) - else: - # Will get picked up by _dispatch_debuglink_decision eventually - debuglink_decision_chan.publish((debug_events.last_event, msg)) + try: + # click on specific coordinates, with possible hold + if x is not None and y is not None: + await _layout_click(x, y, msg.hold_ms or 0) + # press specific button + elif msg.physical_button is not None: + await _layout_press_button(msg.physical_button, msg.hold_ms or 0) + elif msg.swipe is not None: + await _layout_swipe(msg.swipe) + elif msg.button is not None: + await _layout_event(msg.button) + elif msg.input is not None: + ui.CURRENT_LAYOUT._emit_message(msg.input) + else: + raise RuntimeError("Invalid DebugLinkDecision message") - if msg.wait: - # We wait for all the previously sent events - debug_events.awaited_event = debug_events.last_event - storage.layout_watcher = LAYOUT_WATCHER_LAYOUT - loop.schedule(return_layout_change()) + except ui.Shutdown: + # Shutdown should be raised if the layout is supposed to stop after + # processing the event. In that case, we need to yield to give the layout + # callers time to finish their jobs. We want to make sure that the handling + # does not continue until the event is truly processed. + result = await layout_change_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 layout_is_ready(): + return await return_layout_change(DEBUG_CONTEXT, detect_deadlock=True) else: - m.tokens = storage.current_content_tokens - - return m + return _state() async def dispatch_DebugLinkRecordScreen(msg: DebugLinkRecordScreen) -> Success: if msg.target_directory: + # Ensure we consistently start at a layout, instead of randomly sometimes + # hitting the pause between layouts and rendering the "upcoming" one. + await wait_until_layout_is_running() + # In case emulator is restarted but we still want to record screenshots # into the same directory as before, we need to increment the refresh index, # so that the screenshots are not overwritten. @@ -236,6 +297,14 @@ if __debug__: REFRESH_INDEX = msg.refresh_index storage.save_screen_directory = msg.target_directory storage.save_screen = True + + # force repaint current layout, in order to take an initial screenshot + # (doing it this way also clears the red square, because the repaint is + # happening with screenshotting already enabled) + assert isinstance(ui.CURRENT_LAYOUT, ui.Layout) + ui.CURRENT_LAYOUT.request_complete_repaint() + ui.CURRENT_LAYOUT._paint() + else: storage.save_screen = False display.clear_save() # clear C buffers @@ -271,26 +340,6 @@ if __debug__: sdcard.power_off() return Success() - def boot() -> None: - register = workflow_handlers.register # local_cache_attribute - - register(MessageType.DebugLinkDecision, dispatch_DebugLinkDecision) # type: ignore [Argument of type "(msg: DebugLinkDecision) -> Coroutine[Any, Any, None]" cannot be assigned to parameter "handler" of type "Handler[Msg@register]" in function "register"] - register(MessageType.DebugLinkGetState, dispatch_DebugLinkGetState) # type: ignore [Argument of type "(msg: DebugLinkGetState) -> Coroutine[Any, Any, DebugLinkState | None]" cannot be assigned to parameter "handler" of type "Handler[Msg@register]" in function "register"] - register(MessageType.DebugLinkReseedRandom, dispatch_DebugLinkReseedRandom) - register(MessageType.DebugLinkRecordScreen, dispatch_DebugLinkRecordScreen) - register(MessageType.DebugLinkEraseSdCard, dispatch_DebugLinkEraseSdCard) - register(MessageType.DebugLinkWatchLayout, dispatch_DebugLinkWatchLayout) - register( - MessageType.DebugLinkResetDebugEvents, dispatch_DebugLinkResetDebugEvents - ) - register( - MessageType.DebugLinkOptigaSetSecMax, dispatch_DebugLinkOptigaSetSecMax - ) - - loop.schedule(debuglink_decision_dispatcher()) - if storage.layout_watcher is not LAYOUT_WATCHER_NONE: - loop.schedule(return_layout_change()) - async def dispatch_DebugLinkOptigaSetSecMax( msg: DebugLinkOptigaSetSecMax, ) -> Success: @@ -301,3 +350,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/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index 7e1dc3d0ae..743d554de6 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -20,7 +20,6 @@ if TYPE_CHECKING: EthereumTokenInfo, EthereumTxAck, ) - from trezor.ui.layouts.common import ProgressLayout from apps.common.keychain import Keychain @@ -40,7 +39,9 @@ async def sign_tx( keychain: Keychain, defs: Definitions, ) -> EthereumTxRequest: + from trezor import TR from trezor.crypto.hashlib import sha3_256 + from trezor.ui.layouts.progress import progress from trezor.utils import HashWriter from apps.common import paths @@ -69,9 +70,8 @@ async def sign_tx( ) await confirm_tx_data(msg, defs, address_bytes, maximum_fee, fee_items, data_total) - _start_progress() - - _render_progress(30) + progress_obj = progress(title=TR.progress__signing_transaction) + progress_obj.report(30) # sign data = bytearray() @@ -95,7 +95,7 @@ async def sign_tx( rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, data) sha.extend(data) - _render_progress(60) + progress_obj.report(60) while data_left > 0: resp = await send_request_chunk(data_left) @@ -110,7 +110,7 @@ async def sign_tx( digest = sha.get_digest() result = _sign_digest(msg, keychain, digest) - _finish_progress() + progress_obj.stop() return result @@ -393,30 +393,3 @@ async def _handle_staking_tx_claim( await require_confirm_claim( staking_addr, msg.address_n, maximum_fee, fee_items, network, chunkify ) - - -_progress_obj: ProgressLayout | None = None - - -def _start_progress() -> None: - from trezor import TR, workflow - from trezor.ui.layouts.progress import progress - - global _progress_obj - - if not utils.DISABLE_ANIMATION: - # Because we are drawing to the screen manually, without a layout, we - # should make sure that no other layout is running. - workflow.close_others() - _progress_obj = progress(title=TR.progress__signing_transaction) - - -def _render_progress(progress: int) -> None: - global _progress_obj - if _progress_obj is not None: - _progress_obj.report(progress) - - -def _finish_progress() -> None: - global _progress_obj - _progress_obj = None diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index 3f29f1b42e..4dcd7646e5 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 88c8dcbf4d..bd0fe0b346 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 db2f0a5880..07115c83d2 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -23,12 +23,11 @@ if TYPE_CHECKING: async def request_mnemonic( word_count: int, backup_type: BackupType | None ) -> str | None: - from trezor.ui.layouts.common import button_request from trezor.ui.layouts.recovery import request_word from . import word_validity - await button_request("mnemonic", code=ButtonRequestType.MnemonicInput) + send_button_request = True # Pre-allocate the list to enable going back and overwriting words. words: list[str] = [""] * word_count @@ -43,8 +42,10 @@ async def request_mnemonic( i, word_count, is_slip39=backup_types.is_slip39_word_count(word_count), + send_button_request=send_button_request, prefill_word=words[i], ) + send_button_request = False if not word: # User has decided to go back diff --git a/core/src/apps/misc/get_firmware_hash.py b/core/src/apps/misc/get_firmware_hash.py index 9f40a15723..67a01ba53a 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 056dc56f54..ab712e1d19 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 2df18c35a2..01777b7a9c 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -68,7 +68,7 @@ async def bootscreen() -> None: lockscreen = Lockscreen( label=storage.device.get_label(), bootscreen=True ) - await lockscreen + await lockscreen.get_result() lockscreen.__del__() await verify_user_pin() storage.init_unlocked() diff --git a/core/src/storage/debug.py b/core/src/storage/debug.py index a8064294ad..e674ee92ec 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 0000000000..3c650960c5 --- /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 7db42bbe86..a724b1159b 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -517,6 +517,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 00b307b741..856bc7c971 100644 --- a/core/src/trezor/loop.py +++ b/core/src/trezor/loop.py @@ -671,24 +671,3 @@ class spawn(Syscall): is True, it would be calling close on self, which will result in a ValueError. """ return self.task is this_task - - -class Timer(Syscall): - def __init__(self) -> None: - self.task: Task | None = None - # Event::Attach is evaluated before task is set. Use this list to - # buffer timers until task is set. - self.before_task: list[tuple[int, Any]] = [] - - def handle(self, task: Task) -> None: - self.task = task - for deadline, value in self.before_task: - schedule(self.task, value, deadline) - self.before_task.clear() - - def schedule(self, deadline: int, value: Any) -> None: - deadline = utime.ticks_add(utime.ticks_ms(), deadline) - if self.task is not None: - schedule(self.task, value, deadline) - else: - self.before_task.append((deadline, value)) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 5a5bb25245..ce92ae6cae 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 @@ -2809,7 +2810,6 @@ if TYPE_CHECKING: input: "str | None" x: "int | None" y: "int | None" - wait: "bool | None" hold_ms: "int | None" physical_button: "DebugPhysicalButton | None" @@ -2821,7 +2821,6 @@ if TYPE_CHECKING: input: "str | None" = None, x: "int | None" = None, y: "int | None" = None, - wait: "bool | None" = None, hold_ms: "int | None" = None, physical_button: "DebugPhysicalButton | None" = None, ) -> None: @@ -2831,20 +2830,6 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkDecision"]: return isinstance(msg, cls) - class DebugLinkLayout(protobuf.MessageType): - tokens: "list[str]" - - def __init__( - self, - *, - tokens: "list[str] | None" = None, - ) -> None: - pass - - @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkLayout"]: - return isinstance(msg, cls) - class DebugLinkReseedRandom(protobuf.MessageType): value: "int | None" @@ -2876,16 +2861,12 @@ if TYPE_CHECKING: return isinstance(msg, cls) class DebugLinkGetState(protobuf.MessageType): - wait_word_list: "bool | None" - wait_word_pos: "bool | None" - wait_layout: "bool | None" + wait_layout: "DebugWaitType" def __init__( self, *, - wait_word_list: "bool | None" = None, - wait_word_pos: "bool | None" = None, - wait_layout: "bool | None" = None, + wait_layout: "DebugWaitType | None" = None, ) -> None: pass @@ -3031,26 +3012,6 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkEraseSdCard"]: return isinstance(msg, cls) - class DebugLinkWatchLayout(protobuf.MessageType): - watch: "bool | None" - - def __init__( - self, - *, - watch: "bool | None" = None, - ) -> None: - pass - - @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkWatchLayout"]: - return isinstance(msg, cls) - - class DebugLinkResetDebugEvents(protobuf.MessageType): - - @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["DebugLinkResetDebugEvents"]: - return isinstance(msg, cls) - class DebugLinkOptigaSetSecMax(protobuf.MessageType): @classmethod diff --git a/core/src/trezor/pin.py b/core/src/trezor/pin.py index 27bbd04865..bc3b113d42 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 87bc11908c..5a31e87a7a 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -1,21 +1,30 @@ # pylint: disable=wrong-import-position import utime +from micropython import const from trezorui import Display -from typing import TYPE_CHECKING, Any, Awaitable, Generator +from typing import TYPE_CHECKING -from trezor import loop, utils -from trezorui2 import AttachType, BacklightLevels +import trezorui2 +from trezor import io, log, loop, utils, wire, workflow +from trezor.messages import ButtonAck, ButtonRequest +from trezor.wire import context +from trezorui2 import BacklightLevels, LayoutState if TYPE_CHECKING: - from typing import Generic, TypeVar + from typing import Any, Callable, Generator, Generic, Iterator, TypeVar - from trezorui2 import UiResult # noqa: F401 + from trezorui2 import AttachType, LayoutObj, UiResult # noqa: F401 - T = TypeVar("T") + T = TypeVar("T", covariant=True) else: - Generic = [object] T = 0 + Generic = {T: object} + + +if __debug__: + trezorui2.disable_animation(bool(utils.DISABLE_ANIMATION)) + # all rendering is done through a singleton of `Display` display = Display() @@ -28,15 +37,14 @@ MONO: int = Display.FONT_MONO WIDTH: int = Display.WIDTH HEIGHT: int = Display.HEIGHT -# channel used to cancel layouts, see `Cancelled` exception -layout_chan = loop.chan() +_REQUEST_ANIMATION_FRAME = const(1) +"""Animation frame timer token. +See `trezor::ui::layout::base::EventCtx::ANIM_FRAME_TIMER`. +""" # allow only one alert at a time to avoid alerts overlapping _alert_in_progress = False -# storing last transition type, so that next layout can continue nicely -LAST_TRANSITION_OUT: AttachType | None = None - # in debug mode, display an indicator in top right corner if __debug__: @@ -100,102 +108,406 @@ def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None: display.backlight(val) -class Result(Exception): +class Shutdown(Exception): + pass + + +SHUTDOWN = Shutdown() + +CURRENT_LAYOUT: "Layout | ProgressLayout | None" = None + + +def set_current_layout(layout: "Layout | ProgressLayout | None") -> None: + """Set the current global layout. + + All manipulation of the global `CURRENT_LAYOUT` MUST go through this function. + It ensures that the transitions are always to/from None (so that there are never + two layouts in RUNNING state), and that the debug UI is notified of the change. """ - When components want to trigger layout completion, they do so through - raising an instance of `Result`. + global CURRENT_LAYOUT - See `Layout.__iter__` for details. - """ + # all transitions must be to/from None + assert (CURRENT_LAYOUT is None) == (layout is not None) - def __init__(self, value: Any) -> None: - super().__init__() - self.value = value - - -class Cancelled(Exception): - """ - Layouts can be explicitly cancelled. This usually happens when another - layout starts, because only one layout can be running at the same time, - and is done by raising `Cancelled` on the cancelled layout. Layouts - should always re-raise such exceptions. - - See `Layout.__iter__` for details. - """ + CURRENT_LAYOUT = layout class Layout(Generic[T]): + """Python-side handler and runner for the Rust based layouts. + + Wrap a `LayoutObj` instance in `Layout` to be able to display the layout, run its + event loop, and take part in global layout management. See + [docs/core/misc/layout-lifecycle.md] for details. """ - Abstract class. - - Layouts are top-level components. Only one layout can be running at the - same time. Layouts provide asynchronous interface, so a running task can - wait for the layout to complete. Layouts complete when a `Result` is - raised, usually from some of the child components. - """ - - def finalize(self) -> None: - """ - Called when the layout is done. Usually overridden to allow cleanup or storing context. - """ - pass - - async def __iter__(self) -> T: - """ - Run the layout and wait until it completes. Returns the result value. - Usually not overridden. - """ - if __debug__: - # we want to call notify_layout_change() when the rendering is done; - # but only the first time the layout is awaited. Here we indicate that we - # are being awaited, and in handle_rendering() we send the appropriate event - self.should_notify_layout_change = True - - value = None - try: - # If any other layout is running (waiting on the layout channel), - # we close it with the Cancelled exception, and wait until it is - # closed, just to be sure. - if layout_chan.takers: - await layout_chan.put(Cancelled()) - # Now, no other layout should be running. In a loop, we create new - # layout tasks and execute them in parallel, while waiting on the - # layout channel. This allows other layouts to cancel us, and the - # layout tasks to trigger restart by exiting (new tasks are created - # and we continue, because we are in a loop). - while True: - await loop.race(layout_chan.take(), *self.create_tasks()) - except Result as result: - # Result exception was raised, this means this layout is complete. - value = result.value - finally: - self.finalize() - return value - - if TYPE_CHECKING: - - def __await__(self) -> Generator[Any, Any, T]: - return self.__iter__() # type: ignore [Coroutine[Any, Any, T@Layout]" is incompatible with "Generator[Any, Any, T@Layout]"] - - else: - __await__ = __iter__ - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - """ - Called from `__iter__`. Creates and returns a sequence of tasks that - run this layout. Tasks are executed in parallel. When one of them - returns, the others are closed and `create_tasks` is called again. - - Usually overridden to add another tasks to the list.""" - raise NotImplementedError if __debug__: - def read_content_into(self, content_store: list[str]) -> None: - content_store.clear() - content_store.append(self.__class__.__name__) + @staticmethod + def _trace(layout: LayoutObj) -> str: + tokens = [] + + def callback(*args: str) -> None: + tokens.extend(args) + + layout.trace(callback) + return "".join(tokens) + + def __str__(self) -> str: + return f"{repr(self)}({self._trace(self.layout)[:150]})" + + @staticmethod + def notify_debuglink(layout: "Layout | None") -> None: + from apps.debug import notify_layout_change + + notify_layout_change(layout) + + def __init__(self, layout: LayoutObj[T]) -> None: + """Set up a layout.""" + self.layout = layout + self.tasks: set[loop.Task] = set() + self.timers: dict[int, loop.Task] = {} + self.result_box = loop.mailbox() + self.button_request_box = loop.mailbox() + self.transition_out: AttachType | None = None + self.backlight_level = BacklightLevels.NORMAL + self.context: context.Context | None = None + self.state: LayoutState = LayoutState.INITIAL + + def is_ready(self) -> bool: + """True if the layout is in READY state.""" + return CURRENT_LAYOUT is not self and self.result_box.is_empty() + + def is_running(self) -> bool: + """True if the layout is in RUNNING state.""" + return CURRENT_LAYOUT is self + + def is_stopped(self) -> bool: + """True if the layout is in STOPPED state.""" + return CURRENT_LAYOUT is not self and not self.result_box.is_empty() + + def is_layout_attached(self) -> bool: + return self.state is LayoutState.ATTACHED + + def start(self) -> None: + """Start the layout, stopping any other RUNNING layout. + + If the layout is already RUNNING, do nothing. If the layout is STOPPED, fail. + """ + global CURRENT_LAYOUT + + # do nothing if we are already running + if self.is_running(): + return + + # make sure we are not restarted before picking the previous result + assert self.is_ready() + + transition_in = None + + # set up the global layout, shutting down any competitors + # (caller should still call `workflow.close_others()` to ensure that someone + # else will not just shut us down immediately) + if CURRENT_LAYOUT is not None: + prev_layout = CURRENT_LAYOUT + prev_layout.stop() + transition_in = prev_layout.transition_out + + assert CURRENT_LAYOUT is None + # do not notify debuglink, we will do it when we receive an ATTACHED event + set_current_layout(self) + + # save context + self.context = context.CURRENT_CONTEXT + + # attach a timer callback and paint self + self._event(self.layout.attach_timer_fn, self._set_timer, transition_in) + + # spawn all tasks + for task in self.create_tasks(): + self._start_task(task) + + def stop(self, _kill_taker: bool = True) -> None: + """Stop the layout, moving out of RUNNING state and unsetting self as the + current layout. + + The resulting state is either READY (if there is no result to be picked up) or + STOPPED. + + When called externally, this kills any tasks that wait for the result, assuming + that the external `stop()` is a kill. When called internally, `_kill_taker` is + set to False to indicate that a result became available and that the taker + should be allowed to pick it up. + """ + global CURRENT_LAYOUT + + # stop all running timers and spawned tasks + for timer in self.timers.values(): + loop.close(timer) + for task in self.tasks: + if task != loop.this_task: + loop.close(task) + self.timers.clear() + self.tasks.clear() + + self.transition_out = self.layout.get_transition_out() + + # shut down anyone who is waiting for the result + if _kill_taker: + self.result_box.maybe_close() + + if CURRENT_LAYOUT is self: + # fade to black -- backlight is off while no layout is running + backlight_fade(BacklightLevels.NONE) + + set_current_layout(None) + if __debug__: + self.notify_debuglink(None) + + async def get_result(self) -> T: + """Wait for, and return, the result of this UI layout.""" + if self.is_ready(): + self.start() + # else we are (a) still running or (b) already stopped + try: + if self.context is not None and self.result_box.is_empty(): + self._start_task(self._handle_usb_iface()) + return await self.result_box + finally: + self.stop() + + def request_complete_repaint(self) -> None: + """Request a complete repaint of the layout.""" + msg = self.layout.request_complete_repaint() + assert msg is None + + def _event(self, event_call: Callable[..., LayoutState | None], *args: Any) -> None: + """Process an event coming out of the Rust layout. Set is as a result and shut + down the layout if appropriate, do nothing otherwise.""" + if __debug__ and CURRENT_LAYOUT is not self: + raise wire.FirmwareError("layout received an event but it is not running") + + state = event_call(*args) + self.transition_out = self.layout.get_transition_out() + + if state is LayoutState.DONE: + self._emit_message(self.layout.return_value()) + + elif state is LayoutState.ATTACHED: + self._button_request() + if __debug__: + self.notify_debuglink(self) + + if state is not None: + self.state = state + + if state is LayoutState.ATTACHED: + self._first_paint() + else: + self._paint() + + def _button_request(self) -> None: + """Process a button request coming out of the Rust layout.""" + if __debug__ and not self.button_request_box.is_empty(): + raise wire.FirmwareError( + "button request already pending -- " + "don't forget to yield your input flow from time to time ^_^" + ) + + res = self.layout.button_request() + if res is None: + return + + if self.context is None: + return + + # in production, we don't want this to fail, hence replace=True + self.button_request_box.put(res, replace=True) + + def _paint(self) -> None: + """Paint the layout and ensure that homescreen cache is properly invalidated.""" + import storage.cache as storage_cache + + painted = self.layout.paint() + if painted: + refresh() + if storage_cache.homescreen_shown is not None and painted: + storage_cache.homescreen_shown = None + + def _first_paint(self) -> None: + """Paint the layout for the first time after starting it. + + This is a separate call in order for homescreens to be able to override and not + paint when the screen contents are still valid. + """ + # Clear the screen of any leftovers. + self.request_complete_repaint() + self._paint() + + # Turn the brightness on. + backlight_fade(self.backlight_level) + + def _set_timer(self, token: int, duration_ms: int) -> None: + """Timer callback for Rust layouts.""" + + async def timer_task() -> None: + self.timers.pop(token) + try: + self._event(self.layout.timer, token) + except Shutdown: + pass + + if token == _REQUEST_ANIMATION_FRAME and token in self.timers: + # do not schedule another animation frame if one is already scheduled + return + + assert token not in self.timers + task = timer_task() + self.timers[token] = task + deadline = utime.ticks_add(utime.ticks_ms(), duration_ms) + loop.schedule(task, deadline=deadline) + + def _emit_message(self, msg: Any) -> None: + """Process a message coming out of the Rust layout. Set is as a result and shut + down the layout if appropriate, do nothing otherwise.""" + # when emitting a message, there should not be another one already waiting + assert self.result_box.is_empty() + self.stop(_kill_taker=False) + self.result_box.put(msg) + raise SHUTDOWN + + def create_tasks(self) -> Iterator[loop.Task]: + """Set up background tasks for a layout. + + Called from `start()`. Creates and yields a list of background tasks, typically + event handlers for different interfaces. + + Override and then `yield from super().create_tasks()` to add more tasks.""" + if utils.USE_BUTTON: + yield self._handle_input_iface(io.BUTTON, self.layout.button_event) + if utils.USE_TOUCH: + yield self._handle_input_iface(io.TOUCH, self.layout.touch_event) + + def _handle_input_iface( + self, iface: int, event_call: Callable[..., LayoutState | None] + ) -> Generator: + """Task that is waiting for the user input.""" + touch = loop.wait(iface) + try: + while True: + # Using `yield` instead of `await` to avoid allocations. + event = yield touch + workflow.idle_timer.touch() + self._event(event_call, *event) + except Shutdown: + return + finally: + touch.close() + + async def _handle_usb_iface(self) -> None: + if self.context is None: + return + while True: + try: + br_code, br_name = await loop.race( + self.context.read(()), + self.button_request_box, + ) + + await self.context.call( + ButtonRequest( + code=br_code, pages=self.layout.page_count(), name=br_name + ), + ButtonAck, + ) + except Exception: + raise + + def _task_finalizer(self, task: loop.Task, value: Any) -> None: + if value is None: + # all is good + if __debug__: + log.debug(__name__, "UI task exited by itself: %s", task) + return + + if isinstance(value, GeneratorExit): + if __debug__: + log.debug(__name__, "UI task was stopped: %s", task) + return + + if isinstance(value, BaseException): + if __debug__ and value.__class__.__name__ != "UnexpectedMessage": + log.error( + __name__, "UI task died: %s (%s)", task, value.__class__.__name__ + ) + try: + self._emit_message(value) + except Shutdown: + pass + + if __debug__: + log.error(__name__, "UI task returned non-None: %s (%s)", task, value) + + def _start_task(self, task: loop.Task) -> None: + self.tasks.add(task) + loop.schedule(task, finalizer=self._task_finalizer) + + def __del__(self) -> None: + self.layout.__del__() -def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-return-type] - while not layout_chan.takers: - yield # type: ignore [awaitable-return-type] +class ProgressLayout: + """Progress layout. + + Simplified version of the general Layout object, for the purpose of showing spinners + and loaders that are shown "in the background" of a running workflow. Does not run + background tasks, does not respond to timers. + + Participates in global layout management. This is to track whether the progress bar + is currently displayed, who needs to redraw and when. + """ + + def __init__(self, layout: LayoutObj[UiResult]) -> None: + self.layout = layout + self.transition_out = None + + def is_layout_attached(self) -> bool: + return True + + def report(self, value: int, description: str | None = None) -> None: + """Report a progress step. + + Starts the layout if it is not running. + + `value` can be in range from 0 to 1000. + """ + if CURRENT_LAYOUT is not self: + self.start() + + if utils.DISABLE_ANIMATION: + return + + msg = self.layout.progress_event(value, description or "") + assert msg is None + if self.layout.paint(): + refresh() + + def start(self) -> None: + global CURRENT_LAYOUT + + if CURRENT_LAYOUT is not self and CURRENT_LAYOUT is not None: + CURRENT_LAYOUT.stop() + + assert CURRENT_LAYOUT is None + set_current_layout(self) + + self.layout.request_complete_repaint() + painted = self.layout.paint() + backlight_fade(BacklightLevels.NONE) + if painted: + refresh() + + def stop(self) -> None: + global CURRENT_LAYOUT + + if CURRENT_LAYOUT is self: + set_current_layout(None) diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py index 1f17d7d02f..9047cd0a3c 100644 --- a/core/src/trezor/ui/layouts/common.py +++ b/core/src/trezor/ui/layouts/common.py @@ -1,42 +1,98 @@ from typing import TYPE_CHECKING -from trezor import workflow +import trezorui2 +from trezor import ui, utils, workflow from trezor.enums import ButtonRequestType from trezor.messages import ButtonAck, ButtonRequest -from trezor.wire import context +from trezor.wire import ActionCancelled, context if TYPE_CHECKING: - from typing import Awaitable, Protocol, TypeVar + from typing import Any, Awaitable, Callable, TypeVar - T = TypeVar("T") - - LayoutType = Awaitable PropertyType = tuple[str | None, str | bytes | None] ExceptionType = BaseException | type[BaseException] - class ProgressLayout(Protocol): - def report(self, value: int, description: str | None = None) -> None: ... + InfoFunc = Callable[[], Awaitable[None]] + + T = TypeVar("T") -async def button_request( +async def _button_request( br_name: str, code: ButtonRequestType = ButtonRequestType.Other, - pages: int | None = None, + pages: int = 0, ) -> None: workflow.close_others() await context.maybe_call( - ButtonRequest(code=code, pages=pages, name=br_name), ButtonAck + ButtonRequest(code=code, pages=pages or None, name=br_name), ButtonAck ) async def interact( - layout: LayoutType[T], - br_name: str, + layout_obj: ui.LayoutObj[T], + br_name: str | None, br_code: ButtonRequestType = ButtonRequestType.Other, + raise_on_cancel: ExceptionType | None = ActionCancelled, ) -> T: - pages = None - if hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access attribute "page_count" for class "LayoutType"] - # We know for certain how many pages the layout will have - pages = layout.page_count() # type: ignore [Cannot access attribute "page_count" for class "LayoutType"] - await button_request(br_name, br_code, pages) - return await layout + # shut down other workflows to prevent them from interfering with the current one + workflow.close_others() + # start the layout + layout = ui.Layout(layout_obj) + layout.start() + # send the button request + if br_name is not None: + await _button_request(br_name, br_code, layout_obj.page_count()) + # wait for the layout result + result = await layout.get_result() + # raise an exception if the user cancelled the action + if raise_on_cancel is not None and result is trezorui2.CANCELLED: + raise raise_on_cancel + return result + + +def raise_if_not_confirmed( + layout_obj: ui.LayoutObj[ui.UiResult], + br_name: str | None, + br_code: ButtonRequestType = ButtonRequestType.Other, + exc: ExceptionType = ActionCancelled, +) -> Awaitable[None]: + action = interact(layout_obj, br_name, br_code, exc) + return action # type: ignore ["UiResult" is incompatible with "None"] + + +async def with_info( + main_layout: ui.LayoutObj[ui.UiResult], + info_layout: ui.LayoutObj[Any], + br_name: str, + br_code: ButtonRequestType, +) -> None: + send_button_request = True + + while True: + result = await interact( + main_layout, br_name if send_button_request else None, br_code + ) + # raises on cancel + send_button_request = False + + if result is trezorui2.CONFIRMED: + return + elif result is trezorui2.INFO: + await interact(info_layout, None, raise_on_cancel=None) + continue + else: + raise RuntimeError # unexpected result + + +def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None: + # Simple drawing not supported for layouts that set timers. + def dummy_set_timer(token: int, duration: int) -> None: + raise RuntimeError + + layout.attach_timer_fn(dummy_set_timer, None) + if utils.USE_BACKLIGHT: + ui.backlight_fade(ui.BacklightLevels.DIM) + if layout.paint(): + ui.refresh() + if utils.USE_BACKLIGHT: + ui.backlight_fade(ui.BacklightLevels.NORMAL) diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py index ef13c57a4d..1e15e4ed92 100644 --- a/core/src/trezor/ui/layouts/homescreen.py +++ b/core/src/trezor/ui/layouts/homescreen.py @@ -1,8 +1,133 @@ -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) + self.context = None + + def _paint(self) -> None: + if self.layout.paint(): + ui.refresh() + + def _first_paint(self) -> None: + if storage_cache.homescreen_shown is not self.RENDER_INDICATOR: + super()._first_paint() + storage_cache.homescreen_shown = self.RENDER_INDICATOR + # else: + # self._paint() + + +class Homescreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.HOMESCREEN_ON + + def __init__( + self, + label: str | None, + notification: str | None, + notification_is_error: bool, + hold_to_lock: bool, + ) -> None: + level = 1 + if notification is not None: + notification = notification.rstrip( + "!" + ) # TODO handle TS5 that doesn't have it + if notification == TR.homescreen__title_coinjoin_authorized: + level = 3 + elif notification == TR.homescreen__title_experimental_mode: + level = 2 + elif notification_is_error: + level = 0 + + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_homescreen( + label=label, + notification=notification, + notification_level=level, + hold=hold_to_lock, + skip_first_paint=skip, + ), + ) + + async def usb_checker_task(self) -> None: + from trezor import io, loop + + usbcheck = loop.wait(io.USB_CHECK) + while True: + is_connected = await usbcheck + self._event(self.layout.usb_event, is_connected) + + def create_tasks(self) -> Iterator[loop.Task]: + yield from super().create_tasks() + yield self.usb_checker_task() + + +class Lockscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON + + def __init__( + self, + label: str | None, + bootscreen: bool = False, + coinjoin_authorized: bool = False, + ) -> None: + self.bootscreen = bootscreen + self.backlight_level = ui.BacklightLevels.LOW + if bootscreen: + self.backlight_level = ui.BacklightLevels.NORMAL + + skip = ( + not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR + ) + super().__init__( + layout=trezorui2.show_lockscreen( + label=label, + bootscreen=bootscreen, + skip_first_paint=skip, + coinjoin_authorized=coinjoin_authorized, + ), + ) + + async def get_result(self) -> Any: + result = await super().get_result() + if self.bootscreen: + self.request_complete_repaint() + return result + + +class Busyscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON + + def __init__(self, delay_ms: int) -> None: + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_progress_coinjoin( + title=TR.coinjoin__waiting_for_others, + indeterminate=True, + time_ms=delay_ms, + skip_first_paint=skip, + ) + ) + + async def get_result(self) -> Any: + from apps.base import set_homescreen + + # Handle timeout. + result = await super().get_result() + assert result == trezorui2.CANCELLED + storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS) + set_homescreen() + return result diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index 9e819f44b0..504bfc6541 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType from trezor.messages import ButtonAck, ButtonRequest from trezor.wire import ActionCancelled, context -from ..common import button_request, interact +from ..common import draw_simple, interact, raise_if_not_confirmed, with_info if TYPE_CHECKING: from typing import Any, Awaitable, Iterable, NoReturn, Sequence, TypeVar @@ -23,271 +23,6 @@ CANCELLED = trezorui2.CANCELLED INFO = trezorui2.INFO -if __debug__: - from trezor.utils import DISABLE_ANIMATION - - trezorui2.disable_animation(bool(DISABLE_ANIMATION)) - - -class RustLayout(ui.Layout): - - # pylint: disable=super-init-not-called - def __init__(self, layout: Any): - self.br_chan = loop.chan() - self.layout = layout - self.timer = loop.Timer() - self._attach() - self._send_button_request() - self.backlight_level = ui.BacklightLevels.NORMAL - - def _attach(self) -> None: - self.layout.attach_timer_fn(self.set_timer, ui.LAST_TRANSITION_OUT) - - def __del__(self): - self.layout.__del__() - - def set_timer(self, token: int, duration_ms: int) -> None: - self.timer.schedule(duration_ms, 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: - - 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) - - # Fade brightness to desired level - 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() - ) - 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 finalize(self): - ui.LAST_TRANSITION_OUT = self.layout.get_transition_out() - - -def draw_simple(layout: Any) -> None: - # Simple drawing not supported for layouts that set timers. - def dummy_set_timer(token: int, duration: int) -> None: - raise RuntimeError - - layout.attach_timer_fn(dummy_set_timer, None) - ui.backlight_fade(ui.BacklightLevels.DIM) - layout.paint() - ui.refresh() - 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, @@ -309,25 +44,21 @@ 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, - prompt_screen=prompt_screen, - prompt_title=prompt_title or title, - ) - ), - 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, + prompt_screen=prompt_screen, + prompt_title=prompt_title or title, ), + br_name, + br_code, exc, ) @@ -348,31 +79,23 @@ def confirm_single( 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, ) def confirm_reset_device(_title: str, recovery: bool = False) -> Awaitable[None]: - return raise_if_not_confirmed( - RustLayout(trezorui2.flow_confirm_reset(recovery=recovery)) - ) + return raise_if_not_confirmed(trezorui2.flow_confirm_reset(recovery=recovery), None) async def show_wallet_created_success() -> None: await interact( - RustLayout( - trezorui2.show_success(title=TR.backup__new_wallet_created, description="") - ), + trezorui2.show_success(title=TR.backup__new_wallet_created, description=""), "backup_device", ButtonRequestType.ResetDevice, ) @@ -380,9 +103,10 @@ async def show_wallet_created_success() -> None: async def prompt_backup() -> bool: result = await interact( - RustLayout(trezorui2.flow_prompt_backup()), + trezorui2.flow_prompt_backup(), "backup_device", ButtonRequestType.ResetDevice, + raise_on_cancel=None, ) return result is CONFIRMED @@ -397,30 +121,22 @@ def confirm_path_warning( else f"{TR.words__unknown} {path_type.lower()}." ) return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.flow_warning_hi_prio( - title=f"{TR.words__warning}!", description=description, value=path - ) - ), - "path_warning", - br_code=ButtonRequestType.UnknownDerivationPath, - ) + trezorui2.flow_warning_hi_prio( + title=f"{TR.words__warning}!", description=description, value=path + ), + "path_warning", + br_code=ButtonRequestType.UnknownDerivationPath, ) def confirm_multisig_warning() -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.flow_warning_hi_prio( - title=f"{TR.words__important}!", - description=TR.send__receiving_to_multisig, - ) - ), - "warning_multisig", - br_code=ButtonRequestType.Warning, - ) + trezorui2.flow_warning_hi_prio( + title=f"{TR.words__important}!", + description=TR.send__receiving_to_multisig, + ), + "warning_multisig", + br_code=ButtonRequestType.Warning, ) @@ -433,16 +149,12 @@ def confirm_homescreen( workflow.close_others() 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, ) @@ -521,23 +233,22 @@ async def show_address( ) await raise_if_not_confirmed( - RustLayout( - trezorui2.flow_get_address( - address=address, - title=title or TR.address__title_receive_address, - description=network or "", - extra=None, - chunkify=chunkify, - address_qr=address if address_qr is None else address_qr, - case_sensitive=case_sensitive, - account=account, - path=path, - xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], - title_success=title_success, - br_name=br_name, - br_code=br_code, - ) - ) + trezorui2.flow_get_address( + address=address, + title=title or TR.address__title_receive_address, + description=network or "", + extra=None, + chunkify=chunkify, + address_qr=address if address_qr is None else address_qr, + case_sensitive=case_sensitive, + account=account, + path=path, + xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], + title_success=title_success, + br_name=br_name, + br_code=br_code, + ), + None, ) @@ -573,16 +284,15 @@ 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, ) raise exc @@ -596,18 +306,14 @@ def show_warning( ) -> Awaitable[None]: button = button or TR.buttons__continue # def_arg return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=TR.words__important, - value=content, - button=subheader or TR.words__continue_anyway_question, - danger=True, - ) - ), - br_name, - br_code, - ) + trezorui2.show_warning( + title=TR.words__important, + value=content, + button=subheader or TR.words__continue_anyway_question, + danger=True, + ), + br_name, + br_code, ) @@ -618,16 +324,12 @@ def show_success( button: str | None = None, ) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_success( - title=content, - description=subheader if subheader else "", - ) - ), - br_name, - ButtonRequestType.Success, - ) + trezorui2.show_success( + title=content, + description=subheader if subheader else "", + ), + br_name, + ButtonRequestType.Success, ) @@ -654,28 +356,27 @@ async def confirm_output( title = TR.send__title_sending_to await raise_if_not_confirmed( - RustLayout( - trezorui2.flow_confirm_output( - title=TR.words__address, - subtitle=title, - message=address, - amount=amount, - chunkify=chunkify, - text_mono=True, - account=source_account, - account_path=source_account_path, - address=None, - address_title=None, - br_code=br_code, - br_name="confirm_output", - summary_items=None, - fee_items=None, - summary_title=None, - summary_br_name=None, - summary_br_code=None, - cancel_text=cancel_text, - ) - ) + trezorui2.flow_confirm_output( + title=TR.words__address, + subtitle=title, + message=address, + amount=amount, + chunkify=chunkify, + text_mono=True, + account=source_account, + account_path=source_account_path, + address=None, + address_title=None, + br_code=br_code, + br_name="confirm_output", + summary_items=None, + fee_items=None, + summary_title=None, + summary_br_name=None, + summary_br_code=None, + cancel_text=cancel_text, + ), + br_name=None, ) @@ -690,14 +391,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, @@ -729,13 +428,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, @@ -757,7 +454,6 @@ 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): from ubinascii import hexlify @@ -772,18 +468,15 @@ 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) + result = await interact( + trezorui2.confirm_more( + title=title, + button=TR.buttons__close, + items=[(ui.MONO, data)], + ), + br_name, + br_code, + ) assert result in (CONFIRMED, CANCELLED) assert False @@ -802,18 +495,16 @@ def confirm_blob( chunkify: bool = False, prompt_screen: bool = True, ) -> Awaitable[None]: - layout = RustLayout( - trezorui2.confirm_blob( - title=title, - description=description, - data=data, - extra=None, - hold=hold, - verb=verb, - verb_cancel=verb_cancel, - chunkify=chunkify, - prompt_screen=prompt_screen, - ) + layout = trezorui2.confirm_blob( + title=title, + description=description, + data=data, + extra=None, + hold=hold, + verb=verb, + verb_cancel=verb_cancel, + chunkify=chunkify, + prompt_screen=prompt_screen, ) if ask_pagination and layout.page_count() > 1: @@ -822,11 +513,9 @@ def confirm_blob( else: return raise_if_not_confirmed( - interact( - layout, - br_name, - br_code, - ) + layout, + br_name, + br_code, ) @@ -903,32 +592,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, ) @@ -943,17 +626,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, ) @@ -987,17 +666,16 @@ def confirm_total( fee_items.append((TR.confirm_total__fee_rate, fee_rate_amount)) return raise_if_not_confirmed( - RustLayout( - trezorui2.flow_confirm_summary( - title=title, - items=items, - fee_items=fee_items, - account_items=account_items, - br_name=br_name, - br_code=br_code, - cancel_text=TR.send__cancel_sign, - ) - ) + trezorui2.flow_confirm_summary( + title=title, + items=items, + fee_items=fee_items, + account_items=account_items, + br_name=br_name, + br_code=br_code, + cancel_text=TR.send__cancel_sign, + ), + None, ) @@ -1015,17 +693,16 @@ def _confirm_summary( title = title or TR.words__title_summary # def_arg return raise_if_not_confirmed( - RustLayout( - trezorui2.flow_confirm_summary( - title=title, - items=items or (), - fee_items=fee_items or (), - account_items=info_items or (), - br_name=br_name, - br_code=br_code, - cancel_text=cancel_text, - ) - ) + trezorui2.flow_confirm_summary( + title=title, + items=items or (), + fee_items=fee_items or (), + account_items=info_items or (), + br_name=br_name, + br_code=br_code, + cancel_text=cancel_text, + ), + None, ) @@ -1043,31 +720,30 @@ if not utils.BITCOIN_ONLY: chunkify: bool = False, ) -> None: await raise_if_not_confirmed( - RustLayout( - trezorui2.flow_confirm_output( - title=TR.words__address, - subtitle=TR.words__recipient, - message=recipient, - amount=None, - chunkify=chunkify, - text_mono=True, - account=account, - account_path=account_path, - address=None, - address_title=None, - br_code=ButtonRequestType.Other, - br_name="confirm_output", - summary_items=( - (TR.words__amount, total_amount), - (TR.send__maximum_fee, maximum_fee), - ), - fee_items=fee_info_items, - summary_title=TR.words__title_summary, - summary_br_name="confirm_total", - summary_br_code=ButtonRequestType.SignTx, - cancel_text=TR.buttons__cancel, - ) - ) + trezorui2.flow_confirm_output( + title=TR.words__address, + subtitle=TR.words__recipient, + message=recipient, + amount=None, + chunkify=chunkify, + text_mono=True, + account=account, + account_path=account_path, + address=None, + address_title=None, + br_code=ButtonRequestType.Other, + br_name="confirm_output", + summary_items=( + (TR.words__amount, total_amount), + (TR.send__maximum_fee, maximum_fee), + ), + fee_items=fee_info_items, + summary_title=TR.words__title_summary, + summary_br_name="confirm_total", + summary_br_code=ButtonRequestType.SignTx, + cancel_text=TR.buttons__cancel, + ), + None, ) async def confirm_ethereum_staking_tx( @@ -1093,28 +769,27 @@ if not utils.BITCOIN_ONLY: (TR.send__maximum_fee, maximum_fee), ) await raise_if_not_confirmed( - RustLayout( - trezorui2.flow_confirm_output( - title=verb, - subtitle=None, - message=intro_question, - amount=None, - chunkify=False, - text_mono=False, - account=account, - account_path=account_path, - br_code=br_code, - br_name=br_name, - address=address, - address_title=address_title, - summary_items=summary_items, - fee_items=info_items, - summary_title=verb, - summary_br_name="confirm_total", - summary_br_code=ButtonRequestType.SignTx, - cancel_text=TR.buttons__cancel, # cancel staking - ) - ) + trezorui2.flow_confirm_output( + title=verb, + subtitle=None, + message=intro_question, + amount=None, + chunkify=False, + text_mono=False, + account=account, + account_path=account_path, + br_code=br_code, + br_name=br_name, + address=address, + address_title=address_title, + summary_items=summary_items, + fee_items=info_items, + summary_title=verb, + summary_br_name="confirm_total", + summary_br_code=ButtonRequestType.SignTx, + cancel_text=TR.buttons__cancel, # cancel staking + ), + br_name=None, ) def confirm_solana_tx( @@ -1189,70 +864,39 @@ 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", - ButtonRequestType.ConfirmOutput, - address_layout.page_count(), - ) - address_layout.request_complete_repaint() - await raise_if_not_confirmed(address_layout) - - if send_button_request: - send_button_request = False - await button_request( - "modify_output", - ButtonRequestType.ConfirmOutput, - modify_layout.page_count(), - ) - modify_layout.request_complete_repaint() - result = await modify_layout + await raise_if_not_confirmed( + address_layout, + "modify_output" if send_button_request else None, + ButtonRequestType.ConfirmOutput, + ) + result = await interact( + modify_layout, + "modify_output" if send_button_request else None, + ButtonRequestType.ConfirmOutput, + raise_on_cancel=None, + ) + send_button_request = False if result is CONFIRMED: break -async def with_info( - main_layout: RustLayout, - info_layout: RustLayout, - br_name: str, - br_code: ButtonRequestType, -) -> Any: - await button_request(br_name, br_code, pages=main_layout.page_count()) - - while True: - result = await main_layout - - if result is INFO: - info_layout.request_complete_repaint() - result = await info_layout - assert result is CANCELLED - main_layout.request_complete_repaint() - continue - else: - return result - - def confirm_modify_fee( title: str, sign: int, @@ -1260,41 +904,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, ) @@ -1326,15 +960,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]] = [] @@ -1349,23 +981,19 @@ 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: @@ -1373,26 +1001,21 @@ async def confirm_signverify( 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) + result = await interact( + trezorui2.show_mismatch(title=TR.addr_mismatch__mismatch), 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 + continue - message_layout.request_complete_repaint() result = await interact(message_layout, br_name, BR_CODE_OTHER) 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, @@ -1400,20 +1023,18 @@ def show_error_popup( *, button: str = "", timeout_ms: int = 0, -) -> Awaitable[None]: +) -> ui.LayoutObj[ui.UiResult]: if not button and not timeout_ms: raise ValueError("Either button or timeout_ms must be set") if subtitle: title += f"\n{subtitle}" - return RustLayout( - trezorui2.show_error( - title=title, - description=description.format(description_param), - button=button, - time_ms=timeout_ms, - allow_cancel=False, - ) + return trezorui2.show_error( + title=title, + description=description.format(description_param), + button=button, + time_ms=timeout_ms, + allow_cancel=False, ) @@ -1430,29 +1051,24 @@ def show_wait_text(message: str) -> None: draw_simple(trezorui2.show_wait_text(message)) -async def request_passphrase_on_device(max_len: int) -> str: - result = await interact( - RustLayout( - trezorui2.flow_request_passphrase( - prompt=TR.passphrase__title_enter, max_len=max_len - ) +def request_passphrase_on_device(max_len: int) -> Awaitable[str]: + result = interact( + trezorui2.flow_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 + return result # type: ignore ["UiResult" is incompatible with "str"] -async def request_pin_on_device( +def request_pin_on_device( prompt: str, attempts_remaining: int | None, allow_cancel: bool, wrong_pin: bool = False, -) -> str: +) -> Awaitable[str]: from trezor.wire import PinCancelled if attempts_remaining is None: @@ -1462,51 +1078,49 @@ async def request_pin_on_device( else: 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, - ) + result = interact( + 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 + return result # type: ignore ["UiResult" is incompatible with "str"] -async def confirm_reenter_pin( - is_wipe_code: bool = False, -) -> None: +async def confirm_reenter_pin(is_wipe_code: bool = False) -> None: """Not supported for Mercury.""" 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[ui.UiResult]: title = TR.wipe_code__mismatch if is_wipe_code else TR.pin__mismatch description = TR.wipe_code__enter_new if is_wipe_code else TR.pin__reenter_new - 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, ) -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[ui.UiResult]: + 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, ) @@ -1518,33 +1132,25 @@ def confirm_set_new_pin( br_code: ButtonRequestType = BR_CODE_OTHER, ) -> Awaitable[None]: return raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.flow_confirm_set_new_pin(title=title, description=description) - ), - br_name, - br_code, - ) + trezorui2.flow_confirm_set_new_pin(title=title, description=description), + 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)), +def set_brightness(current: int | None = None) -> Awaitable[None]: + return raise_if_not_confirmed( + trezorui2.set_brightness(current=current), "set_brightness", BR_CODE_OTHER, ) @@ -1553,9 +1159,7 @@ async def set_brightness(current: int | None = None) -> None: def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]: """Showing users how to interact with the device.""" return raise_if_not_confirmed( - interact( - RustLayout(trezorui2.tutorial()), - "tutorial", - br_code, - ) + trezorui2.tutorial(), + "tutorial", + br_code, ) diff --git a/core/src/trezor/ui/layouts/mercury/fido.py b/core/src/trezor/ui/layouts/mercury/fido.py index 83f7e987db..d74df0619e 100644 --- a/core/src/trezor/ui/layouts/mercury/fido.py +++ b/core/src/trezor/ui/layouts/mercury/fido.py @@ -1,54 +1,10 @@ from typing import TYPE_CHECKING import trezorui2 +from trezor import ui from trezor.enums import ButtonRequestType from ..common import interact -from . import RustLayout - -if TYPE_CHECKING: - from trezor.loop import AwaitableTask - -if __debug__: - from trezor import io, ui - - from ... import Result - - # needed solely for test_emu_u2f - class _RustFidoLayoutImpl(RustLayout): - def create_tasks(self) -> tuple[AwaitableTask, ...]: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_swipe(), - self.handle_click_signal(), - self.handle_debug_confirm(), - ) - - async def handle_debug_confirm(self) -> None: - from apps.debug import result_signal - - _event_id, result = await result_signal() - if result is not trezorui2.CONFIRMED: - raise Result(result) - - for event, x, y in ( - (io.TOUCH_START, 120, 160), - (io.TOUCH_MOVE, 120, 130), - (io.TOUCH_END, 120, 100), - (io.TOUCH_START, 120, 120), - (io.TOUCH_END, 120, 120), - ): - msg = self.layout.touch_event(event, x, y) - if self.layout.paint(): - ui.refresh() - if msg is not None: - raise Result(msg) - - _RustFidoLayout = _RustFidoLayoutImpl - -else: - _RustFidoLayout = RustLayout async def confirm_fido( @@ -58,16 +14,30 @@ async def confirm_fido( accounts: list[str | None], ) -> int: """Webauthn confirmation for one or more credentials.""" - confirm = _RustFidoLayout( - trezorui2.confirm_fido( - title=header, - app_name=app_name, - icon_name=icon_name, - accounts=accounts, - ) + confirm = trezorui2.confirm_fido( + title=header, + app_name=app_name, + icon_name=icon_name, + accounts=accounts, ) result = await interact(confirm, "confirm_fido", ButtonRequestType.Other) + if __debug__ and result is trezorui2.CONFIRMED: + # debuglink will directly inject a CONFIRMED message which we need to handle + # by playing back a click to the Rust layout and getting out the selected number + # that way + from trezor import io + + msg = confirm.touch_event(io.TOUCH_START, 220, 220) + assert msg is None + if confirm.paint(): + ui.refresh() + msg = confirm.touch_event(io.TOUCH_END, 220, 220) + if confirm.paint(): + ui.refresh() + assert isinstance(msg, int) + return msg + # The Rust side returns either an int or `CANCELLED`. We detect the int situation # and assume cancellation otherwise. if isinstance(result, int): @@ -82,7 +52,7 @@ async def confirm_fido( async def confirm_fido_reset() -> bool: from trezor import TR - confirm = RustLayout( + confirm = ui.Layout( trezorui2.confirm_action( title=TR.fido__title_reset, action=TR.fido__erase_credentials, @@ -91,4 +61,4 @@ async def confirm_fido_reset() -> bool: prompt_screen=True, ) ) - return (await confirm) is trezorui2.CONFIRMED + return (await confirm.get_result()) is trezorui2.CONFIRMED diff --git a/core/src/trezor/ui/layouts/mercury/homescreen.py b/core/src/trezor/ui/layouts/mercury/homescreen.py deleted file mode 100644 index b24fd3e253..0000000000 --- a/core/src/trezor/ui/layouts/mercury/homescreen.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import TYPE_CHECKING - -import storage.cache as storage_cache -import trezorui2 -from trezor import TR, ui - -from . import RustLayout - -if TYPE_CHECKING: - from typing import Any, Tuple - - from trezor import loop - - -class HomescreenBase(RustLayout): - RENDER_INDICATOR: object | None = None - - def __init__(self, layout: Any) -> None: - super().__init__(layout=layout) - - def _attach(self) -> None: - if storage_cache.homescreen_shown is self.RENDER_INDICATOR: - self.layout.attach_timer_fn(self.set_timer, ui.AttachType.RESUME) - else: - self.layout.attach_timer_fn(self.set_timer, ui.LAST_TRANSITION_OUT) - - def _paint(self) -> None: - if self.layout.paint(): - ui.refresh() - - def _first_paint(self) -> None: - if storage_cache.homescreen_shown is not self.RENDER_INDICATOR: - super()._first_paint() - storage_cache.homescreen_shown = self.RENDER_INDICATOR - else: - self._paint() - - if __debug__: - # In __debug__ mode, ignore {confirm,swipe,input}_signal. - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - return ( - self.handle_input_and_rendering(), - self.handle_timers(), - self.handle_click_signal(), # so we can receive debug events - ) - - -class Homescreen(HomescreenBase): - RENDER_INDICATOR = storage_cache.HOMESCREEN_ON - - def __init__( - self, - label: str | None, - notification: str | None, - notification_is_error: bool, - hold_to_lock: bool, - ) -> None: - level = 1 - if notification is not None: - if notification == TR.homescreen__title_coinjoin_authorized: - level = 3 - elif notification == TR.homescreen__title_experimental_mode: - level = 2 - elif notification_is_error: - level = 0 - - skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR - super().__init__( - layout=trezorui2.show_homescreen( - label=label, - notification=notification, - notification_level=level, - hold=hold_to_lock, - skip_first_paint=skip, - ), - ) - - async def usb_checker_task(self) -> None: - from trezor import io, loop - - usbcheck = loop.wait(io.USB_CHECK) - while True: - is_connected = await usbcheck - self.layout.usb_event(is_connected) - if self.layout.paint(): - ui.refresh() - - def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]: - return super().create_tasks() + (self.usb_checker_task(),) - - -class Lockscreen(HomescreenBase): - RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON - - def __init__( - self, - label: str | None, - bootscreen: bool = False, - coinjoin_authorized: bool = False, - ) -> None: - self.bootscreen = bootscreen - self.backlight_level = ui.BacklightLevels.LOW - if bootscreen: - self.backlight_level = ui.BacklightLevels.NORMAL - - skip = ( - not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR - ) - super().__init__( - layout=trezorui2.show_lockscreen( - label=label, - bootscreen=bootscreen, - skip_first_paint=skip, - coinjoin_authorized=coinjoin_authorized, - ), - ) - - async def __iter__(self) -> Any: - result = await super().__iter__() - if self.bootscreen: - self.request_complete_repaint() - return result - - -class Busyscreen(HomescreenBase): - RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON - - def __init__(self, delay_ms: int) -> None: - from trezor import TR - - skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR - super().__init__( - layout=trezorui2.show_progress_coinjoin( - title=TR.coinjoin__waiting_for_others, - indeterminate=True, - time_ms=delay_ms, - skip_first_paint=skip, - ) - ) - - async def __iter__(self) -> Any: - from apps.base import set_homescreen - - # Handle timeout. - result = await super().__iter__() - assert result == trezorui2.CANCELLED - storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS) - set_homescreen() - return result diff --git a/core/src/trezor/ui/layouts/mercury/recovery.py b/core/src/trezor/ui/layouts/mercury/recovery.py index c765744202..f7e4e628a1 100644 --- a/core/src/trezor/ui/layouts/mercury/recovery.py +++ b/core/src/trezor/ui/layouts/mercury/recovery.py @@ -1,11 +1,11 @@ from typing import TYPE_CHECKING import trezorui2 -from trezor import TR +from trezor import TR, ui from trezor.enums import ButtonRequestType, RecoveryType from ..common import interact -from . import RustLayout, raise_if_not_confirmed +from . import raise_if_not_confirmed CONFIRMED = trezorui2.CONFIRMED # global_import_cache CANCELLED = trezorui2.CANCELLED # global_import_cache @@ -16,30 +16,36 @@ if TYPE_CHECKING: async def request_word_count(recovery_type: RecoveryType) -> int: - selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type)) - count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount) + selector = trezorui2.select_word_count(recovery_type=recovery_type) + count = await interact( + selector, "recovery_word_count", ButtonRequestType.MnemonicWordCount + ) return int(count) async def request_word( - word_index: int, word_count: int, is_slip39: bool, prefill_word: str = "" + word_index: int, + word_count: int, + is_slip39: bool, + send_button_request: bool, + prefill_word: str = "", ) -> str: prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count) can_go_back = word_index > 0 if is_slip39: - keyboard = RustLayout( - trezorui2.request_slip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) + keyboard = trezorui2.request_slip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back ) else: - keyboard = RustLayout( - trezorui2.request_bip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) + keyboard = trezorui2.request_bip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back ) - word: str = await keyboard + word: str = await interact( + keyboard, + "mnemonic" if send_button_request else None, + ButtonRequestType.MnemonicInput, + ) return word @@ -78,20 +84,16 @@ def format_remaining_shares_info( async def show_group_share_success(share_index: int, group_index: int) -> None: await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_group_share_success( - lines=[ - TR.recovery__you_have_entered, - TR.recovery__share_num_template.format(share_index + 1), - TR.words__from, - TR.recovery__group_num_template.format(group_index + 1), - ], - ) - ), - "share_success", - ButtonRequestType.Other, - ) + trezorui2.show_group_share_success( + lines=[ + TR.recovery__you_have_entered, + TR.recovery__share_num_template.format(share_index + 1), + TR.words__from, + TR.recovery__group_num_template.format(group_index + 1), + ], + ), + "share_success", + ButtonRequestType.Other, ) @@ -103,9 +105,7 @@ async def continue_recovery( show_info: bool = False, remaining_shares_info: "RemainingSharesInfo | None" = None, ) -> bool: - # NOTE: show_info can be understood as first screen before any shares - # NOTE: button request sent from the flow - result = await RustLayout( + result = await interact( trezorui2.flow_continue_recovery( first_screen=show_info, recovery_type=recovery_type, @@ -116,7 +116,10 @@ async def continue_recovery( if remaining_shares_info else None ), - ) + ), + None, + ButtonRequestType.Other, + raise_on_cancel=None, ) return result is CONFIRMED @@ -130,17 +133,13 @@ async def show_recovery_warning( ) -> None: button = button or TR.buttons__try_again # def_arg await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=content or TR.words__warning, - value=subheader or "", - button=button, - description="", - danger=True, - ) - ), - br_name, - br_code, - ) + trezorui2.show_warning( + title=content or TR.words__warning, + value=subheader or "", + button=button, + description="", + danger=True, + ), + br_name, + br_code, ) diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 0efa5855b0..ae18af1dd6 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -1,27 +1,21 @@ -from typing import TYPE_CHECKING +from typing import Awaitable, Callable, Sequence import trezorui2 -from trezor import TR +from trezor import TR, ui from trezor.enums import ButtonRequestType from trezor.wire import ActionCancelled from ..common import interact -from . import RustLayout, raise_if_not_confirmed, show_success - -if TYPE_CHECKING: - pass - from typing import Callable, Sequence - +from . import raise_if_not_confirmed, show_success CONFIRMED = trezorui2.CONFIRMED # global_import_cache -async def show_share_words( +def show_share_words( share_words: Sequence[str], share_index: int | None = None, group_index: int | None = None, -) -> None: - +) -> Awaitable[None]: title = TR.reset__recovery_wallet_backup_title if share_index is None: subtitle = "" @@ -43,7 +37,7 @@ async def show_share_words( text_info.append(TR.reset__repeat_for_all_shares) text_confirm = TR.reset__words_written_down_template.format(words_count) - result = await RustLayout( + return raise_if_not_confirmed( trezorui2.flow_show_share_words( title=title, subtitle=subtitle, @@ -51,12 +45,10 @@ async def show_share_words( description=description, text_info=text_info, text_confirm=text_confirm, - ) + ), + None, ) - if result != CONFIRMED: - raise ActionCancelled - async def select_word( words: Sequence[str], @@ -81,14 +73,15 @@ async def select_word( while len(words) < 3: words.append(words[-1]) - result = await RustLayout( + result = await interact( trezorui2.select_word( title=title, description=TR.reset__select_word_x_of_y_template.format( checked_index + 1, count ), words=(words[0], words[1], words[2]), - ) + ), + None, ) if __debug__ and isinstance(result, str): return result @@ -104,13 +97,11 @@ async def slip39_show_checklist( ) -> None: items = _slip_39_checklist_items(step, advanced, count, threshold) result = await interact( - RustLayout( - trezorui2.show_checklist( - title=TR.reset__title_shamir_backup, - button=TR.buttons__continue, - active=step, - items=items, - ) + trezorui2.show_checklist( + title=TR.reset__title_shamir_backup, + button=TR.buttons__continue, + active=step, + items=items, ), "slip39_checklist", ButtonRequestType.ResetDevice, @@ -162,8 +153,7 @@ async def _prompt_number( max_count: int, br_name: str, ) -> int: - - result = await RustLayout( + result = await interact( trezorui2.flow_request_number( title=title, description=description, @@ -173,7 +163,8 @@ async def _prompt_number( info=info, br_code=ButtonRequestType.ResetDevice, br_name=br_name, - ) + ), + None, ) if __debug__ and result is CONFIRMED: @@ -188,9 +179,9 @@ async def _prompt_number( raise ActionCancelled # user cancelled request number prompt -async def slip39_prompt_threshold( +def slip39_prompt_threshold( num_of_shares: int, group_id: int | None = None -) -> int: +) -> Awaitable[int]: count = num_of_shares // 2 + 1 # min value of share threshold is 2 unless the number of shares is 1 # number of shares 1 is possible in advanced slip39 @@ -212,7 +203,7 @@ async def slip39_prompt_threshold( ) ) - return await _prompt_number( + return _prompt_number( TR.reset__title_set_threshold, description, info, @@ -298,63 +289,53 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non description = TR.backup__info_multi_share_backup await interact( - RustLayout( - trezorui2.show_info( - title=TR.backup__title_create_wallet_backup, description=description - ) + trezorui2.show_info( + title=TR.backup__title_create_wallet_backup, description=description + ), + "backup_intro", + ButtonRequestType.ResetDevice, + ) + + +def show_warning_backup() -> Awaitable[ui.UiResult]: + return interact( + trezorui2.show_warning( + title=TR.words__important, + value=TR.reset__never_make_digital_copy, + button="", + allow_cancel=False, + danger=False, # Use a less severe icon color ), "backup_warning", ButtonRequestType.ResetDevice, ) -async def show_warning_backup() -> None: - result = await interact( - RustLayout( - trezorui2.show_warning( - title=TR.words__important, - value=TR.reset__never_make_digital_copy, - button="", - allow_cancel=False, - danger=False, # Use a less severe icon color - ) - ), - "backup_warning", - ButtonRequestType.ResetDevice, - ) - if result != CONFIRMED: - raise ActionCancelled - - -async def show_success_backup() -> None: - await show_success( +def show_success_backup() -> Awaitable[None]: + return show_success( "success_backup", TR.backup__title_backup_completed, ) -async def show_reset_warning( +def show_reset_warning( br_name: str, content: str, subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> None: - await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=subheader or "", - description=content, - value="", - button="", - allow_cancel=False, - danger=True, - ) - ), - br_name, - br_code, - ) +) -> Awaitable[None]: + return raise_if_not_confirmed( + trezorui2.show_warning( + title=subheader or "", + description=content, + value="", + button="", + allow_cancel=False, + danger=True, + ), + br_name, + br_code, ) @@ -391,11 +372,11 @@ async def show_share_confirmation_success( ) ) - return await show_success("success_recovery", title, subheader=footer_description) + await show_success("success_recovery", title, subheader=footer_description) -async def show_share_confirmation_failure() -> None: - await show_reset_warning( +def show_share_confirmation_failure() -> Awaitable[None]: + return show_reset_warning( "warning_backup_check", TR.words__try_again, TR.reset__incorrect_word_selected, diff --git a/core/src/trezor/ui/layouts/progress.py b/core/src/trezor/ui/layouts/progress.py index 053acbc3da..fae4c73a9f 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,15 @@ def _storage_message_to_str(message: config.StorageMessage | None) -> str | None raise RuntimeError # unknown message -class RustProgress: - def __init__( - self, - layout: Any, - ): - self.layout = layout - ui.backlight_fade(ui.BacklightLevels.DIM) - self.layout.attach_timer_fn(self.set_timer, None) - if self.layout.paint(): - ui.refresh() - ui.backlight_fade(ui.BacklightLevels.NORMAL) - - def set_timer(self, token: int, duration_ms: int) -> None: - raise RuntimeError # progress layouts should not set timers - - def report(self, value: int, description: str | None = None): - msg = self.layout.progress_event(value, description or "") - assert msg is None - if self.layout.paint(): - ui.refresh() - - def progress( description: str | None = None, title: str | None = None, indeterminate: bool = False, -) -> ProgressLayout: +) -> ui.ProgressLayout: if description is None: description = TR.progress__please_wait # def_arg - return RustProgress( + return ui.ProgressLayout( layout=trezorui2.show_progress( description=description, title=title, @@ -67,27 +38,27 @@ def progress( ) -def bitcoin_progress(message: str) -> ProgressLayout: +def bitcoin_progress(message: str) -> ui.ProgressLayout: return progress(message) -def coinjoin_progress(message: str) -> ProgressLayout: - return RustProgress( +def coinjoin_progress(message: str) -> ui.ProgressLayout: + return ui.ProgressLayout( layout=trezorui2.show_progress_coinjoin(title=message, indeterminate=False) ) -def pin_progress(title: config.StorageMessage, description: str) -> ProgressLayout: +def pin_progress(title: config.StorageMessage, description: str) -> ui.ProgressLayout: return progress(description=description, title=_storage_message_to_str(title)) if not utils.BITCOIN_ONLY: - def monero_keyimage_sync_progress() -> ProgressLayout: + def monero_keyimage_sync_progress() -> ui.ProgressLayout: return progress(TR.progress__syncing) - def monero_live_refresh_progress() -> ProgressLayout: + def monero_live_refresh_progress() -> ui.ProgressLayout: return progress(TR.progress__refreshing, indeterminate=True) - def monero_transaction_progress_inner() -> ProgressLayout: + def monero_transaction_progress_inner() -> ui.ProgressLayout: return progress(TR.progress__signing_transaction) diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 54105ffeb1..b28e48e396 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 draw_simple, 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 @@ -29,293 +20,6 @@ INFO = trezorui2.INFO BR_CODE_OTHER = ButtonRequestType.Other # global_import_cache -if __debug__: - from trezor.utils import DISABLE_ANIMATION - - 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, duration_ms: int) -> None: - self.timer.schedule(duration_ms, 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, duration: int) -> None: - raise RuntimeError - - layout.attach_timer_fn(dummy_set_timer, None) - layout.paint() - ui.refresh() - - # Temporary function, so we know where it is used # Should be gradually replaced by custom designs/layouts def _placeholder_confirm( @@ -343,43 +47,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 +69,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, - subtitle=subtitle, - verb=verb, - verb_cancel=verb_cancel, - hold=hold, - 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, + reverse=reverse, ), + br_name, + br_code, exc, ) @@ -433,8 +96,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 +117,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 +136,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, + 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 +173,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 +183,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, ) @@ -596,26 +252,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: @@ -634,7 +286,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, @@ -643,14 +295,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), + None, + raise_on_cancel=None, + ) assert result in (CONFIRMED, CANCELLED) # Right button aborts action, left goes back to showing address. if result is CONFIRMED: @@ -716,7 +374,9 @@ async def show_error_and_raise( content, button=button, br_code=BR_CODE_OTHER, + exc=None, ) + # always raise regardless of result raise exc @@ -726,7 +386,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 @@ -739,15 +400,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, ) @@ -805,44 +465,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( @@ -879,14 +531,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, @@ -897,8 +547,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( @@ -915,17 +564,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: @@ -933,15 +581,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( @@ -952,12 +593,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, @@ -968,22 +615,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 @@ -1057,17 +689,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, ) @@ -1090,20 +718,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: @@ -1111,30 +736,23 @@ async def confirm_value( send_button_request = True while True: - should_show_more_layout = RustLayout( + result = await interact( trezorui2.confirm_with_info( title=title, items=((ui.NORMAL, value),), button=verb or TR.buttons__confirm, info_button=TR.buttons__info, - ) + ), + br_name if send_button_request else None, + br_code, ) - - if send_button_request: - send_button_request = False - await button_request( - br_name, - br_code, - should_show_more_layout.page_count(), - ) - - result = await should_show_more_layout + send_button_request = False if result is CONFIRMED: return elif result is INFO: info_title, info_value = info_items_list[0] - await RustLayout( + await interact( trezorui2.confirm_blob( title=info_title, data=info_value, @@ -1144,11 +762,13 @@ async def confirm_value( verb_cancel="<", hold=False, chunkify=chunkify_info, - ) + ), + None, + raise_on_cancel=None, ) + continue else: - assert result is CANCELLED - raise ActionCancelled + raise RuntimeError # unexpected result, interact should have raised def confirm_total( @@ -1165,22 +785,18 @@ def confirm_total( ) -> Awaitable[None]: total_label = total_label or f"{TR.send__total_amount}:" # 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, ) @@ -1221,20 +837,16 @@ if not utils.BITCOIN_ONLY: amount_title = f"{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=f"{TR.send__maximum_fee}:", - fee_value=maximum_fee, - items=[(f"{k}:", v) for (k, v) in 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=f"{TR.send__maximum_fee}:", + fee_value=maximum_fee, + items=[(f"{k}:", v) for (k, v) in info_items], + cancel_cross=True, + ), + br_name=br_name, + br_code=br_code, ) def confirm_solana_tx( @@ -1251,20 +863,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( @@ -1278,14 +886,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=f"{TR.send__maximum_fee}:", - fee_value=maximum_fee, - items=[(f"{k}:", v) for (k, v) in fee_info_items], - ) + summary_layout = trezorui2.altcoin_tx_summary( + amount_title=f"{TR.words__amount}:", + amount_value=total_amount, + fee_title=f"{TR.send__maximum_fee}:", + fee_value=maximum_fee, + items=[(f"{k}:", v) for (k, v) in fee_info_items], ) while True: @@ -1299,13 +905,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: @@ -1314,16 +917,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, ) @@ -1361,46 +960,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 @@ -1412,34 +1003,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, ) @@ -1495,7 +1078,7 @@ async def confirm_signverify( break -def show_error_popup( +def error_popup( title: str, description: str, subtitle: str | None = None, @@ -1503,20 +1086,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: @@ -1529,18 +1110,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 @@ -1563,20 +1140,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, + 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 @@ -1605,30 +1179,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 ["UiResult" is incompatible with "None"] def wipe_code_same_as_pin_popup() -> Awaitable[None]: @@ -1677,13 +1248,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 b05e956dbc..7fd6c9786b 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 9bbd74738a..0000000000 --- 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 df1064e19c..776df1a34a 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -1,20 +1,22 @@ -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING import trezorui2 -from trezor import TR +from trezor import TR, ui from trezor.enums import ButtonRequestType, RecoveryType from ..common import interact -from . import RustLayout, raise_if_not_confirmed, show_warning +from . import show_warning if TYPE_CHECKING: + from typing import Awaitable, Iterable + from apps.management.recovery_device.layout import RemainingSharesInfo async def request_word_count(recovery_type: RecoveryType) -> int: count = await interact( - RustLayout(trezorui2.select_word_count(recovery_type=recovery_type)), - "word_count", + trezorui2.select_word_count(recovery_type=recovery_type), + "recovery_word_count", ButtonRequestType.MnemonicWordCount, ) # It can be returning a string (for example for __debug__ in tests) @@ -22,26 +24,31 @@ async def request_word_count(recovery_type: RecoveryType) -> int: async def request_word( - word_index: int, word_count: int, is_slip39: bool, prefill_word: str = "" + word_index: int, + word_count: int, + is_slip39: bool, + send_button_request: bool, + prefill_word: str = "", ) -> str: prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count) can_go_back = word_index > 0 if is_slip39: - word_choice = RustLayout( - trezorui2.request_slip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) - ) - else: - word_choice = RustLayout( - trezorui2.request_bip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) + keyboard = trezorui2.request_slip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back ) - word: str = await word_choice + else: + keyboard = trezorui2.request_bip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back + ) + + word: str = await interact( + keyboard, + "mnemonic" if send_button_request else None, + ButtonRequestType.MnemonicInput, + ) return word @@ -53,22 +60,20 @@ async def show_remaining_shares( raise NotImplementedError -async def show_group_share_success(share_index: int, group_index: int) -> None: - await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_group_share_success( - lines=[ - TR.recovery__you_have_entered, - TR.recovery__share_num_template.format(share_index + 1), - TR.words__from, - TR.recovery__group_num_template.format(group_index + 1), - ], - ) - ), - "share_success", - ButtonRequestType.Other, - ) +def show_group_share_success( + share_index: int, group_index: int +) -> Awaitable[ui.UiResult]: + return interact( + trezorui2.show_group_share_success( + lines=[ + TR.recovery__you_have_entered, + TR.recovery__share_num_template.format(share_index + 1), + TR.words__from, + TR.recovery__group_num_template.format(group_index + 1), + ], + ), + "share_success", + ButtonRequestType.Other, ) @@ -117,26 +122,24 @@ async def continue_recovery( if subtext: text += f"\n\n{subtext}" + homepage = trezorui2.confirm_recovery( + title="", + description=text, + button=button_label, + recovery_type=recovery_type, + info_button=False, + show_info=show_info, # type: ignore [No parameter named "show_info"] + ) while True: - homepage = RustLayout( - trezorui2.confirm_recovery( - title="", - description=text, - button=button_label, - recovery_type=recovery_type, - info_button=False, - show_info=show_info, # type: ignore [No parameter named "show_info"] - ) - ) result = await interact( homepage, "recovery", ButtonRequestType.RecoveryHomepage, + raise_on_cancel=None, ) if result is trezorui2.CONFIRMED: return True - # user has chosen to abort, confirm the choice try: await _confirm_abort(recovery_type != RecoveryType.NormalRecovery) except ActionCancelled: @@ -145,12 +148,12 @@ async def continue_recovery( return False -async def show_recovery_warning( +def show_recovery_warning( br_name: str, content: str, subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> None: +) -> Awaitable[ui.UiResult]: button = button or TR.buttons__try_again # def_arg - await show_warning(br_name, content, subheader, button, br_code) + 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 e1754d1426..f21e2613dd 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -1,12 +1,11 @@ -from typing import Sequence +from typing import Awaitable, Sequence import trezorui2 from trezor import TR from trezor.enums import ButtonRequestType -from trezor.wire import ActionCancelled -from ..common import interact -from . import RustLayout, confirm_action, show_success, show_warning +from ..common import interact, raise_if_not_confirmed +from . import confirm_action, show_success, show_warning CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -45,13 +44,12 @@ async def show_share_words( ) result = await interact( - RustLayout( - trezorui2.show_share_words( # type: ignore [Arguments missing for parameters] - share_words=share_words, # type: ignore [No parameter named "share_words"] - ) + trezorui2.show_share_words( # type: ignore [Arguments missing for parameters] + share_words=share_words, # type: ignore [No parameter named "share_words"] ), br_name, br_code, + raise_on_cancel=None, ) if result is CONFIRMED: break @@ -83,12 +81,13 @@ async def select_word( words.append(words[-1]) word_ordinal = format_ordinal(checked_index + 1) - result = await RustLayout( + result = await interact( trezorui2.select_word( title="", description=TR.reset__select_word_template.format(word_ordinal), words=(words[0].lower(), words[1].lower(), words[2].lower()), - ) + ), + None, ) if __debug__ and isinstance(result, str): return result @@ -96,12 +95,12 @@ async def select_word( return words[result] -async def slip39_show_checklist( +def slip39_show_checklist( step: int, advanced: bool, count: int | None = None, threshold: int | None = None, -) -> None: +) -> Awaitable[None]: items = ( ( TR.reset__slip39_checklist_num_shares, @@ -116,20 +115,16 @@ async def slip39_show_checklist( ) ) - result = await interact( - RustLayout( - trezorui2.show_checklist( - title=TR.reset__slip39_checklist_title, - button=TR.buttons__continue, - active=step, - items=items, - ) + return raise_if_not_confirmed( + trezorui2.show_checklist( + title=TR.reset__slip39_checklist_title, + button=TR.buttons__continue, + active=step, + items=items, ), "slip39_checklist", ButtonRequestType.ResetDevice, ) - if result is not CONFIRMED: - raise ActionCancelled async def _prompt_number( @@ -139,13 +134,11 @@ async def _prompt_number( max_count: int, br_name: str, ) -> int: - num_input = RustLayout( - trezorui2.request_number( - title=title, - count=count, - min_count=min_count, - max_count=max_count, - ) + num_input = trezorui2.request_number( + title=title, + count=count, + min_count=min_count, + max_count=max_count, ) result = await interact( @@ -225,12 +218,12 @@ async def slip39_prompt_number_of_shares( ) -async def slip39_advanced_prompt_number_of_groups() -> int: +def slip39_advanced_prompt_number_of_groups() -> Awaitable[int]: count = 5 min_count = 2 max_count = 16 - return await _prompt_number( + return _prompt_number( TR.reset__title_number_of_groups, count, min_count, @@ -239,12 +232,12 @@ async def slip39_advanced_prompt_number_of_groups() -> int: ) -async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: +def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> Awaitable[int]: count = num_of_groups // 2 + 1 min_count = 1 max_count = num_of_groups - return await _prompt_number( + return _prompt_number( TR.reset__title_group_threshold, count, min_count, @@ -253,15 +246,15 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) -async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: +def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable[None]: if single_share: assert num_of_words is not None description = TR.backup__info_single_share_backup.format(num_of_words) else: description = TR.backup__info_multi_share_backup - await confirm_action( - "backup_warning", + return confirm_action( + "backup_intro", title=TR.backup__title_backup_wallet, verb=TR.buttons__continue, description=description, @@ -270,8 +263,8 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non ) -async def show_warning_backup() -> None: - await show_warning( +def show_warning_backup() -> Awaitable[trezorui2.UiResult]: + return show_warning( "backup_warning", TR.words__title_remember, TR.reset__never_make_digital_copy, @@ -280,8 +273,8 @@ async def show_warning_backup() -> None: ) -async def show_success_backup() -> None: - await confirm_action( +def show_success_backup() -> Awaitable[None]: + return confirm_action( "success_backup", TR.reset__title_backup_is_done, description=TR.words__keep_it_safe, @@ -291,16 +284,16 @@ async def show_success_backup() -> None: ) -async def show_reset_warning( +def show_reset_warning( br_name: str, content: str, subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> None: +) -> Awaitable[trezorui2.UiResult]: button = button or TR.buttons__try_again # def_arg - await show_warning( + return show_warning( br_name, subheader or "", content, diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index ba9d86fec1..6069e2ea0a 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 draw_simple, interact, raise_if_not_confirmed, with_info if TYPE_CHECKING: - from typing import Any, Awaitable, Iterable, NoReturn, Sequence, TypeVar + from typing import 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 @@ -29,266 +20,6 @@ CANCELLED = trezorui2.CANCELLED INFO = trezorui2.INFO -if __debug__: - from trezor.utils import DISABLE_ANIMATION - - 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, duration_ms: int) -> None: - self.timer.schedule(duration_ms, 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, duration: int) -> None: - raise RuntimeError - - layout.attach_timer_fn(dummy_set_timer, None) - ui.backlight_fade(ui.BacklightLevels.DIM) - layout.paint() - ui.refresh() - 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 +41,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 +69,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 +90,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 +107,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 +143,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_question, - button=TR.buttons__continue, - ) - ), - "path_warning", - br_code=ButtonRequestType.UnknownDerivationPath, - ) + trezorui2.show_warning( + title=title, + value=path, + description=TR.words__continue_anyway_question, + button=TR.buttons__continue, + ), + "path_warning", + br_code=ButtonRequestType.UnknownDerivationPath, ) @@ -456,16 +164,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, ) @@ -535,26 +239,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: @@ -572,7 +271,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, @@ -581,12 +280,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: @@ -625,17 +330,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 @@ -648,17 +353,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, ) @@ -670,18 +371,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, ) @@ -711,27 +408,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, @@ -741,12 +435,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 @@ -761,14 +457,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, @@ -800,13 +494,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, @@ -817,7 +509,6 @@ async def should_show_more( elif result is INFO: return True else: - assert result is CANCELLED raise ActionCancelled @@ -828,12 +519,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, @@ -843,19 +539,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 @@ -874,17 +558,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: @@ -892,13 +575,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( @@ -974,32 +651,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, ) @@ -1014,17 +685,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, ) @@ -1073,23 +740,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: @@ -1105,22 +766,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), - (f"{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), + (f"{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=[(f"{k}:", v) for (k, v) in fee_info_items], - ) + info_layout = trezorui2.show_info_with_cancel( + title=TR.confirm_total__title_fee, + items=[(f"{k}:", v) for (k, v) in fee_info_items], ) while True: @@ -1134,13 +791,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, @@ -1211,19 +866,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, ) @@ -1266,68 +917,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, - ) - ) - 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 - - -async def with_info( - main_layout: RustLayout[T], - info_layout: RustLayout[Any], - br_name: str, - br_code: ButtonRequestType, -) -> T: - await button_request(br_name, br_code, pages=main_layout.page_count()) - - while True: - result = await main_layout - - if result is INFO: - info_layout.request_complete_repaint() - result = await info_layout - assert result is CANCELLED - main_layout.request_complete_repaint() continue else: - return result + return def confirm_modify_fee( @@ -1337,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, ) @@ -1403,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]] = [] @@ -1426,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, @@ -1477,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: @@ -1510,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 @@ -1541,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 @@ -1563,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, ) @@ -1591,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 622e199587..05b733af38 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,31 @@ async def confirm_fido( accounts: list[str | None], ) -> int: """Webauthn confirmation for one or more credentials.""" - confirm = _RustFidoLayout( - trezorui2.confirm_fido( - title=header, - app_name=app_name, - icon_name=icon_name, - accounts=accounts, - ) + confirm = trezorui2.confirm_fido( + title=header, + app_name=app_name, + icon_name=icon_name, + accounts=accounts, ) result = await interact(confirm, "confirm_fido", ButtonRequestType.Other) + if __debug__ and result is trezorui2.CONFIRMED: + # debuglink will directly inject a CONFIRMED message which we need to handle + # by playing back a click to the Rust layout and getting out the selected number + # that way + from trezor import io + + confirm.touch_event(io.TOUCH_START, 220, 220) + if confirm.paint(): + ui.refresh() + msg = confirm.touch_event(io.TOUCH_END, 220, 220) + if confirm.paint(): + ui.refresh() + assert msg is trezorui2.LayoutState.DONE + retval = confirm.return_value() + assert isinstance(retval, int) + return retval + # The Rust side returns either an int or `CANCELLED`. We detect the int situation # and assume cancellation otherwise. if isinstance(result, int): @@ -78,7 +51,7 @@ async def confirm_fido( async def confirm_fido_reset() -> bool: from trezor import TR - confirm = RustLayout( + confirm = ui.Layout( trezorui2.confirm_action( title=TR.fido__title_reset, action=TR.fido__erase_credentials, @@ -86,4 +59,4 @@ async def confirm_fido_reset() -> bool: reverse=True, ) ) - return (await confirm) is trezorui2.CONFIRMED + return (await confirm.get_result()) is trezorui2.CONFIRMED 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 7089e8e5fb..0000000000 --- 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 379eeb1f80..79c4ffdab6 100644 --- a/core/src/trezor/ui/layouts/tt/recovery.py +++ b/core/src/trezor/ui/layouts/tt/recovery.py @@ -1,66 +1,60 @@ -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING import trezorui2 -from trezor import TR -from trezor.enums import ButtonRequestType, RecoveryType +from trezor import TR, ui +from trezor.enums import ButtonRequestType from ..common import interact -from . import RustLayout, raise_if_not_confirmed - -CONFIRMED = trezorui2.CONFIRMED # global_import_cache -INFO = trezorui2.INFO # global_import_cache if TYPE_CHECKING: + from typing import Awaitable + + from trezor.enums import RecoveryType + from apps.management.recovery_device.layout import RemainingSharesInfo -async def _homepage_with_info( - dialog: RustLayout, - info_func: Callable, -) -> trezorui2.UiResult: - while True: - result = await dialog - - if result is INFO: - await info_func() - dialog.request_complete_repaint() - else: - return result - - async def request_word_count(recovery_type: RecoveryType) -> int: - selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type)) - count = await interact(selector, "word_count", ButtonRequestType.MnemonicWordCount) + count = await interact( + trezorui2.select_word_count(recovery_type=recovery_type), + "word_count", + ButtonRequestType.MnemonicWordCount, + ) return int(count) async def request_word( - word_index: int, word_count: int, is_slip39: bool, prefill_word: str = "" + word_index: int, + word_count: int, + is_slip39: bool, + send_button_request: bool, + prefill_word: str = "", ) -> str: prompt = TR.recovery__type_word_x_of_y_template.format(word_index + 1, word_count) can_go_back = word_index > 0 if is_slip39: - keyboard = RustLayout( - trezorui2.request_slip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) - ) - else: - keyboard = RustLayout( - trezorui2.request_bip39( - prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back - ) + keyboard = trezorui2.request_slip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back ) - word: str = await keyboard + else: + keyboard = trezorui2.request_bip39( + prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back + ) + + word: str = await interact( + keyboard, + "mnemonic" if send_button_request else None, + ButtonRequestType.MnemonicInput, + ) return word -async def show_remaining_shares( +def show_remaining_shares( groups: set[tuple[str, ...]], shares_remaining: list[int], group_threshold: int, -) -> None: +) -> Awaitable[trezorui2.UiResult]: from trezor import strings from trezor.crypto.slip39 import MAX_SHARE_COUNT @@ -86,31 +80,27 @@ async def show_remaining_shares( words = "\n".join(group) pages.append((title, words)) - await raise_if_not_confirmed( - interact( - RustLayout(trezorui2.show_remaining_shares(pages=pages)), - "show_shares", - ButtonRequestType.Other, - ) + return interact( + trezorui2.show_remaining_shares(pages=pages), + "show_shares", + ButtonRequestType.Other, ) -async def show_group_share_success(share_index: int, group_index: int) -> None: - await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_group_share_success( - lines=[ - TR.recovery__you_have_entered, - TR.recovery__share_num_template.format(share_index + 1), - TR.words__from, - TR.recovery__group_num_template.format(group_index + 1), - ], - ) - ), - "share_success", - ButtonRequestType.Other, - ) +def show_group_share_success( + share_index: int, group_index: int +) -> Awaitable[ui.UiResult]: + return interact( + trezorui2.show_group_share_success( + lines=[ + TR.recovery__you_have_entered, + TR.recovery__share_num_template.format(share_index + 1), + TR.words__from, + TR.recovery__group_num_template.format(group_index + 1), + ], + ), + "share_success", + ButtonRequestType.Other, ) @@ -146,70 +136,60 @@ async def continue_recovery( show_info: bool = False, remaining_shares_info: "RemainingSharesInfo | None" = None, ) -> bool: + from trezor.enums import RecoveryType from trezor.wire import ActionCancelled - from ..common import button_request - - if show_info: + if show_instructions: # Show this just one-time description = TR.recovery__enter_each_word else: description = subtext or "" + homepage = trezorui2.confirm_recovery( + title=text, + description=description, + button=button_label, + recovery_type=recovery_type, + info_button=remaining_shares_info is not None, + ) + while True: - homepage = RustLayout( - trezorui2.confirm_recovery( - title=text, - description=description, - button=button_label, - recovery_type=recovery_type, - info_button=remaining_shares_info is not None, - ) + result = await interact( + homepage, + "recovery", + ButtonRequestType.RecoveryHomepage, + raise_on_cancel=None, ) - await button_request("recovery", ButtonRequestType.RecoveryHomepage) - - if remaining_shares_info is None: - result = await homepage - else: - groups, shares_remaining, group_threshold = remaining_shares_info - result = await _homepage_with_info( - homepage, - lambda: show_remaining_shares( - groups, shares_remaining, group_threshold - ), - ) - - if result is CONFIRMED: + if result is trezorui2.CONFIRMED: return True - - try: - await _confirm_abort(recovery_type != RecoveryType.NormalRecovery) - except ActionCancelled: - pass + elif result is trezorui2.INFO and remaining_shares_info is not None: + await show_remaining_shares(*remaining_shares_info) else: - return False + try: + await _confirm_abort(recovery_type != RecoveryType.NormalRecovery) + except ActionCancelled: + pass + else: + return False -async def show_recovery_warning( +def show_recovery_warning( br_name: str, content: str, subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> None: +) -> Awaitable[ui.UiResult]: button = button or TR.buttons__try_again # def_arg - await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=content, - description=subheader or "", - button=button, - allow_cancel=False, - ) - ), - br_name, - br_code, - ) + + return interact( + trezorui2.show_warning( + title=content, + description=subheader or "", + button=button, + allow_cancel=False, + ), + br_name, + br_code, ) diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index f887d5894a..fd0059b234 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -1,12 +1,10 @@ -from typing import Callable, Sequence +from typing import Awaitable, Callable, Sequence import trezorui2 from trezor import TR from trezor.enums import ButtonRequestType -from trezor.wire import ActionCancelled -from ..common import interact -from . import RustLayout, raise_if_not_confirmed, show_success +from ..common import interact, raise_if_not_confirmed CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -35,11 +33,11 @@ def _split_share_into_pages(share_words: Sequence[str], per_page: int = 4) -> li return pages -async def show_share_words( +def show_share_words( share_words: Sequence[str], share_index: int | None = None, group_index: int | None = None, -) -> None: +) -> Awaitable[None]: if share_index is None: title = TR.reset__recovery_wallet_backup_title elif group_index is None: @@ -51,18 +49,14 @@ async def show_share_words( pages = _split_share_into_pages(share_words) - result = await interact( - RustLayout( - trezorui2.show_share_words( - title=title, - pages=pages, - ), + return raise_if_not_confirmed( + trezorui2.show_share_words( + title=title, + pages=pages, ), "backup_words", ButtonRequestType.ResetDevice, ) - if result != CONFIRMED: - raise ActionCancelled async def select_word( @@ -88,14 +82,15 @@ async def select_word( while len(words) < 3: words.append(words[-1]) - result = await RustLayout( + result = await interact( trezorui2.select_word( title=title, description=TR.reset__select_word_x_of_y_template.format( checked_index + 1, count ), words=(words[0], words[1], words[2]), - ) + ), + None, ) if __debug__ and isinstance(result, str): return result @@ -103,12 +98,12 @@ async def select_word( return words[result] -async def slip39_show_checklist( +def slip39_show_checklist( step: int, advanced: bool, count: int | None = None, threshold: int | None = None, -) -> None: +) -> Awaitable[None]: items = ( ( TR.reset__slip39_checklist_set_num_shares, @@ -123,20 +118,16 @@ async def slip39_show_checklist( ) ) - result = await interact( - RustLayout( - trezorui2.show_checklist( - title=TR.reset__slip39_checklist_title, - button=TR.buttons__continue, - active=step, - items=items, - ) + return raise_if_not_confirmed( + trezorui2.show_checklist( + title=TR.reset__slip39_checklist_title, + button=TR.buttons__continue, + active=step, + items=items, ), "slip39_checklist", ButtonRequestType.ResetDevice, ) - if result != CONFIRMED: - raise ActionCancelled async def _prompt_number( @@ -148,14 +139,12 @@ async def _prompt_number( max_count: int, br_name: str, ) -> int: - num_input = RustLayout( - trezorui2.request_number( - title=title, - description=description, - count=count, - min_count=min_count, - max_count=max_count, - ) + num_input = trezorui2.request_number( + title=title, + description=description, + count=count, + min_count=min_count, + max_count=max_count, ) while True: @@ -163,31 +152,33 @@ async def _prompt_number( num_input, br_name, ButtonRequestType.ResetDevice, + raise_on_cancel=None, ) if __debug__: if not isinstance(result, tuple): # DebugLink currently can't send number of shares and it doesn't # change the counter either so just use the initial value. - result = (result, count) + result = result, count status, value = result if status == CONFIRMED: assert isinstance(value, int) return value - await RustLayout( + await interact( trezorui2.show_simple( title=None, description=info(value), button=TR.buttons__ok_i_understand, - ) + ), + None, + raise_on_cancel=None, ) - num_input.request_complete_repaint() -async def slip39_prompt_threshold( +def slip39_prompt_threshold( num_of_shares: int, group_id: int | None = None -) -> int: +) -> Awaitable[int]: count = num_of_shares // 2 + 1 # min value of share threshold is 2 unless the number of shares is 1 # number of shares 1 is possible in advanced slip39 @@ -230,7 +221,7 @@ async def slip39_prompt_threshold( text += " " + TR.reset__to_form_group_template.format(group_id + 1) return text - return await _prompt_number( + return _prompt_number( TR.reset__title_set_threshold, description, info, @@ -241,9 +232,9 @@ async def slip39_prompt_threshold( ) -async def slip39_prompt_number_of_shares( +def slip39_prompt_number_of_shares( num_words: int, group_id: int | None = None -) -> int: +) -> Awaitable[int]: count = 5 min_count = 1 max_count = 16 @@ -266,7 +257,7 @@ async def slip39_prompt_number_of_shares( num_words, group_id + 1 ) - return await _prompt_number( + return _prompt_number( TR.reset__title_set_number_of_shares, description, lambda i: info, @@ -277,14 +268,14 @@ async def slip39_prompt_number_of_shares( ) -async def slip39_advanced_prompt_number_of_groups() -> int: +def slip39_advanced_prompt_number_of_groups() -> Awaitable[int]: count = 5 min_count = 2 max_count = 16 description = TR.reset__group_description info = TR.reset__group_info - return await _prompt_number( + return _prompt_number( TR.reset__title_set_number_of_groups, lambda i: description, lambda i: info, @@ -295,14 +286,14 @@ async def slip39_advanced_prompt_number_of_groups() -> int: ) -async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: +def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> Awaitable[int]: count = num_of_groups // 2 + 1 min_count = 1 max_count = num_of_groups description = TR.reset__required_number_of_groups info = TR.reset__advanced_group_threshold_info - return await _prompt_number( + return _prompt_number( TR.reset__title_set_group_threshold, lambda i: description, lambda i: info, @@ -313,44 +304,40 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) -async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: +def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable[None]: if single_share: assert num_of_words is not None description = TR.backup__info_single_share_backup.format(num_of_words) else: description = TR.backup__info_multi_share_backup - await interact( - RustLayout( - trezorui2.show_info( - title="", - button=TR.buttons__continue, - description=description, - allow_cancel=False, - ) + return raise_if_not_confirmed( + trezorui2.show_info( + title="", + button=TR.buttons__continue, + description=description, + allow_cancel=False, ), - "backup_warning", + "backup_intro", ButtonRequestType.ResetDevice, ) -async def show_warning_backup() -> None: - result = await interact( - RustLayout( - trezorui2.show_info( - title=TR.reset__never_make_digital_copy, - button=TR.buttons__ok_i_understand, - allow_cancel=False, - ) +def show_warning_backup() -> Awaitable[trezorui2.UiResult]: + return interact( + trezorui2.show_info( + title=TR.reset__never_make_digital_copy, + button=TR.buttons__ok_i_understand, + allow_cancel=False, ), "backup_warning", ButtonRequestType.ResetDevice, ) - if result != CONFIRMED: - raise ActionCancelled async def show_success_backup() -> None: + from . import show_success + await show_success( "success_backup", TR.reset__use_your_backup, @@ -358,27 +345,23 @@ async def show_success_backup() -> None: ) -async def show_reset_warning( +def show_reset_warning( br_name: str, content: str, subheader: str | None = None, button: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Warning, -) -> None: +) -> Awaitable[trezorui2.UiResult]: button = button or TR.buttons__try_again # def_arg - await raise_if_not_confirmed( - interact( - RustLayout( - trezorui2.show_warning( - title=subheader or "", - description=content, - button=button, - allow_cancel=False, - ) - ), - br_name, - br_code, - ) + return interact( + trezorui2.show_warning( + title=subheader or "", + description=content, + button=button, + allow_cancel=False, + ), + br_name, + br_code, ) @@ -387,6 +370,8 @@ async def show_share_confirmation_success( num_of_shares: int | None = None, group_index: int | None = None, ) -> None: + from . import show_success + if share_index is None or num_of_shares is None: # it is a BIP39 or a 1-of-1 SLIP39 backup subheader = TR.reset__finished_verifying_wallet_backup diff --git a/core/src/trezor/wire/context.py b/core/src/trezor/wire/context.py index b2f9afa5e4..e45f609849 100644 --- a/core/src/trezor/wire/context.py +++ b/core/src/trezor/wire/context.py @@ -172,21 +172,6 @@ class Context: CURRENT_CONTEXT: Context | None = None -def wait(task: Awaitable[T]) -> Awaitable[T]: - """ - Wait until the passed in task finishes, and return the result, while servicing the - wire context. - - Used to make sure the device is responsive on USB while waiting for user - interaction. If a message is received before the task finishes, it raises an - `UnexpectedMessage` exception, returning control to the session handler. - """ - if CURRENT_CONTEXT is None: - return task - else: - return loop.race(CURRENT_CONTEXT.read(()), task) - - async def call( msg: protobuf.MessageType, expected_type: type[LoadedMessageType], diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 843bfca6dc..ef26def190 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 Enum, IntEnum, auto @@ -38,19 +39,20 @@ from typing import ( Tuple, Type, Union, - overload, ) from mnemonic import Mnemonic -from typing_extensions import Literal from . import mapping, messages, models, protobuf from .client import TrezorClient from .exceptions import TrezorFailure from .log import DUMP_BYTES +from .messages import DebugWaitType from .tools import expect if TYPE_CHECKING: + from typing_extensions import Protocol + from .messages import PinMatrixRequestType from .transport import Transport @@ -60,6 +62,15 @@ if TYPE_CHECKING: AnyDict = Dict[str, Any] + class InputFunc(Protocol): + def __call__( + self, + hold_ms: Optional[int] = None, + wait: Optional[bool] = None, + ) -> "LayoutContent": + ... + + EXPECTED_RESPONSES_CONTEXT_LINES = 3 LOG = logging.getLogger(__name__) @@ -391,6 +402,29 @@ def multipage_content(layouts: List[LayoutContent]) -> str: return "".join(layout.text_content() for layout in layouts) +def _make_input_func( + button: Optional[messages.DebugButton] = None, + physical_button: Optional[messages.DebugPhysicalButton] = None, + swipe: Optional[messages.DebugSwipeDirection] = None, +) -> "InputFunc": + decision = messages.DebugLinkDecision( + button=button, + physical_button=physical_button, + swipe=swipe, + ) + + def input_func( + self: "DebugLink", + hold_ms: Optional[int] = None, + wait: Optional[bool] = None, + ) -> LayoutContent: + __tracebackhide__ = True # for pytest # pylint: disable=W0612 + decision.hold_ms = hold_ms + return self._decision(decision, wait=wait) + + return input_func # type: ignore [Parameter name mismatch] + + class DebugLink: def __init__(self, transport: "Transport", auto_interact: bool = True) -> None: self.transport = transport @@ -405,7 +439,6 @@ class DebugLink: self.screenshot_recording_dir: Optional[str] = None # For T1 screenshotting functionality in DebugUI - self.t1_take_screenshots = False self.t1_screenshot_directory: Optional[Path] = None self.t1_screenshot_counter = 0 @@ -413,6 +446,11 @@ class DebugLink: self.screen_text_file: Optional[Path] = None self.last_screen_content = "" + self.waiting_for_layout_change = False + self.layout_dirty = True + + self.input_wait_type = DebugWaitType.IMMEDIATE + @property def legacy_ui(self) -> bool: """Differences between UI1 and UI2.""" @@ -439,7 +477,12 @@ class DebugLink: def close(self) -> None: self.transport.end_session() - def _call(self, msg: protobuf.MessageType, nowait: bool = False) -> Any: + def _write(self, msg: protobuf.MessageType) -> None: + if self.waiting_for_layout_change: + raise RuntimeError( + "Debuglink is unavailable while waiting for layout change." + ) + LOG.debug( f"sending message: {msg.__class__.__name__}", extra={"protobuf": msg}, @@ -450,13 +493,12 @@ class DebugLink: f"encoded as type {msg_type} ({len(msg_bytes)} bytes): {msg_bytes.hex()}", ) self.transport.write(msg_type, msg_bytes) - if nowait: - return None + def _read(self) -> protobuf.MessageType: ret_type, ret_bytes = self.transport.read() LOG.log( DUMP_BYTES, - f"received type {msg_type} ({len(msg_bytes)} bytes): {msg_bytes.hex()}", + f"received type {ret_type} ({len(ret_bytes)} bytes): {ret_bytes.hex()}", ) msg = self.mapping.decode(ret_type, ret_bytes) @@ -472,11 +514,20 @@ class DebugLink: ) return msg - def state(self) -> messages.DebugLinkState: - return self._call(messages.DebugLinkGetState()) + def _call(self, msg: protobuf.MessageType) -> Any: + self._write(msg) + return self._read() + + def state( + self, wait_type: DebugWaitType = DebugWaitType.CURRENT_LAYOUT + ) -> messages.DebugLinkState: + result = self._call(messages.DebugLinkGetState(wait_layout=wait_type)) + if isinstance(result, messages.Failure): + raise TrezorFailure(result) + return result def read_layout(self) -> LayoutContent: - return LayoutContent(self.state().tokens or []) + return LayoutContent(self.state().tokens) def wait_layout(self, wait_for_external_change: bool = False) -> LayoutContent: # Next layout change will be caused by external event @@ -487,11 +538,38 @@ class DebugLink: if wait_for_external_change: self.reset_debug_events() - obj = self._call(messages.DebugLinkGetState(wait_layout=True)) + obj = self._call( + messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT) + ) + self.layout_dirty = True if isinstance(obj, messages.Failure): raise TrezorFailure(obj) return LayoutContent(obj.tokens) + @contextmanager + def wait_for_layout_change(self) -> Iterator[LayoutContent]: + # set up a dummy layout content object to be yielded + layout_content = LayoutContent( + ["DUMMY CONTENT, WAIT UNTIL THE END OF THE BLOCK :("] + ) + + # send GetState without waiting for reply + self._write(messages.DebugLinkGetState(wait_layout=DebugWaitType.NEXT_LAYOUT)) + + # allow the block to proceed + self.waiting_for_layout_change = True + try: + yield layout_content + finally: + self.waiting_for_layout_change = False + + # wait for the reply + resp = self._read() + assert isinstance(resp, messages.DebugLinkState) + + # replace contents of the yielded object with the new thing + layout_content.__init__(resp.tokens) + def reset_debug_events(self) -> None: # Only supported on TT and above certain version if (self.model is not models.T1B1) and not self.legacy_debug: @@ -535,56 +613,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: @@ -603,127 +727,8 @@ class DebugLink: f.write(screen_content) f.write("\n" + 80 * "/" + "\n") - # Type overloads below make sure that when we supply `wait=True` into functions, - # they will always return `LayoutContent` and we do not need to assert `is not None`. - - @overload - def click(self, click: Tuple[int, int]) -> None: ... - - @overload - def click(self, click: Tuple[int, int], wait: Literal[True]) -> LayoutContent: ... - - def click( - self, click: Tuple[int, int], wait: bool = False - ) -> Optional[LayoutContent]: - x, y = click - return self.input(x=x, y=y, wait=wait) - - # Made into separate function as `hold_ms: Optional[int]` in `click` - # was causing problems with @overload - def click_hold( - self, click: Tuple[int, int], hold_ms: int - ) -> Optional[LayoutContent]: - x, y = click - return self.input(x=x, y=y, hold_ms=hold_ms, wait=True) - - def press_yes(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input(button=messages.DebugButton.YES, wait=wait) - - def press_no(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input(button=messages.DebugButton.NO, wait=wait) - - def press_info(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input(button=messages.DebugButton.INFO, wait=wait) - - def swipe_up(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input(swipe=messages.DebugSwipeDirection.UP, wait=wait) - - def swipe_down(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input(swipe=messages.DebugSwipeDirection.DOWN, wait=wait) - - @overload - def swipe_right(self) -> None: ... - - @overload - def swipe_right(self, wait: Literal[True]) -> LayoutContent: ... - - def swipe_right(self, wait: bool = False) -> Union[LayoutContent, None]: - return self.input(swipe=messages.DebugSwipeDirection.RIGHT, wait=wait) - - @overload - def swipe_left(self) -> None: ... - - @overload - def swipe_left(self, wait: Literal[True]) -> LayoutContent: ... - - def swipe_left(self, wait: bool = False) -> Union[LayoutContent, None]: - return self.input(swipe=messages.DebugSwipeDirection.LEFT, wait=wait) - - @overload - def press_left(self) -> None: ... - - @overload - def press_left(self, wait: Literal[True]) -> LayoutContent: ... - - def press_left(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input( - physical_button=messages.DebugPhysicalButton.LEFT_BTN, wait=wait - ) - - @overload - def press_middle(self) -> None: ... - - @overload - def press_middle(self, wait: Literal[True]) -> LayoutContent: ... - - def press_middle(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input( - physical_button=messages.DebugPhysicalButton.MIDDLE_BTN, wait=wait - ) - - def press_middle_htc( - self, hold_ms: int, extra_ms: int = 200 - ) -> Optional[LayoutContent]: - return self.press_htc( - button=messages.DebugPhysicalButton.MIDDLE_BTN, - hold_ms=hold_ms, - extra_ms=extra_ms, - ) - - @overload - def press_right(self) -> None: ... - - @overload - def press_right(self, wait: Literal[True]) -> LayoutContent: ... - - def press_right(self, wait: bool = False) -> Optional[LayoutContent]: - return self.input( - physical_button=messages.DebugPhysicalButton.RIGHT_BTN, wait=wait - ) - - def press_right_htc( - self, hold_ms: int, extra_ms: int = 200 - ) -> Optional[LayoutContent]: - return self.press_htc( - button=messages.DebugPhysicalButton.RIGHT_BTN, - hold_ms=hold_ms, - extra_ms=extra_ms, - ) - - def press_htc( - self, button: messages.DebugPhysicalButton, hold_ms: int, extra_ms: int = 200 - ) -> Optional[LayoutContent]: - hold_ms = hold_ms + extra_ms # safety margin - result = self.input( - physical_button=button, - hold_ms=hold_ms, - ) - # sleeping little longer for UI to update - time.sleep(hold_ms / 1000 + 0.1) - return result - def stop(self) -> None: - self._call(messages.DebugLinkStop(), nowait=True) + self._write(messages.DebugLinkStop()) def reseed(self, value: int) -> protobuf.MessageType: return self._call(messages.DebugLinkReseedRandom(value=value)) @@ -757,44 +762,35 @@ class DebugLink: return self._call(messages.DebugLinkMemoryRead(address=address, length=length)) def memory_write(self, address: int, memory: bytes, flash: bool = False) -> None: - self._call( - messages.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash), - nowait=True, + self._write( + messages.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash) ) def flash_erase(self, sector: int) -> None: - self._call(messages.DebugLinkFlashErase(sector=sector), nowait=True) + self._write(messages.DebugLinkFlashErase(sector=sector)) @expect(messages.Success) def erase_sd_card(self, format: bool = True) -> messages.Success: return self._call(messages.DebugLinkEraseSdCard(format=format)) - def take_t1_screenshot_if_relevant(self) -> None: - """Conditionally take screenshots on T1. + def save_screenshot(self, data: bytes) -> None: + if self.t1_screenshot_directory is None: + return - TT handles them differently, see debuglink.start_recording. - """ - if self.model is models.T1B1 and self.t1_take_screenshots: - self.save_screenshot_for_t1() - - def save_screenshot_for_t1(self) -> None: from PIL import Image - layout = self.state().layout - assert layout is not None - assert len(layout) == 128 * 64 // 8 + assert len(data) == 128 * 64 // 8 pixels: List[int] = [] for byteline in range(64 // 8): offset = byteline * 128 - row = layout[offset : offset + 128] + row = data[offset : offset + 128] for bit in range(8): pixels.extend(bool(px & (1 << bit)) for px in row) im = Image.new("1", (128, 64)) im.putdata(pixels[::-1]) - assert self.t1_screenshot_directory is not None img_location = ( self.t1_screenshot_directory / f"{self.t1_screenshot_counter:04d}.png" ) @@ -802,6 +798,9 @@ class DebugLink: self.t1_screenshot_counter += 1 +del _make_input_func + + class NullDebugLink(DebugLink): def __init__(self) -> None: # Ignoring type error as self.transport will not be touched while using NullDebugLink @@ -859,15 +858,9 @@ class DebugUI: self.debuglink.press_yes() def button_request(self, br: messages.ButtonRequest) -> None: - self.debuglink.take_t1_screenshot_if_relevant() + self.debuglink.snapshot() if self.input_flow is None: - # Only calling screen-saver when not in input-flow - # as it collides with wait-layout of input flows. - # All input flows call debuglink.input(), so - # recording their screens that way (as well as - # possible swipes below). - self.debuglink.save_current_screen_if_relevant(wait=True) self._default_input_flow(br) elif self.input_flow is self.INPUT_FLOW_DONE: raise AssertionError("input flow ended prematurely") @@ -879,7 +872,7 @@ class DebugUI: self.input_flow = self.INPUT_FLOW_DONE def get_pin(self, code: Optional["PinMatrixRequestType"] = None) -> str: - self.debuglink.take_t1_screenshot_if_relevant() + self.debuglink.snapshot() if self.pins is None: raise RuntimeError("PIN requested but no sequence was configured") @@ -890,7 +883,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 21db565f45..f206dffcbc 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -556,6 +556,12 @@ class DebugPhysicalButton(IntEnum): RIGHT_BTN = 2 +class DebugWaitType(IntEnum): + IMMEDIATE = 0 + NEXT_LAYOUT = 1 + CURRENT_LAYOUT = 2 + + class EthereumDefinitionType(IntEnum): NETWORK = 0 TOKEN = 1 @@ -4078,7 +4084,7 @@ class DebugLinkGetState(protobuf.MessageType): FIELDS = { 1: protobuf.Field("wait_word_list", "bool", repeated=False, required=False, default=None), 2: protobuf.Field("wait_word_pos", "bool", repeated=False, required=False, default=None), - 3: protobuf.Field("wait_layout", "bool", repeated=False, required=False, default=None), + 3: protobuf.Field("wait_layout", "DebugWaitType", repeated=False, required=False, default=DebugWaitType.IMMEDIATE), } def __init__( @@ -4086,7 +4092,7 @@ class DebugLinkGetState(protobuf.MessageType): *, wait_word_list: Optional["bool"] = None, wait_word_pos: Optional["bool"] = None, - wait_layout: Optional["bool"] = None, + wait_layout: Optional["DebugWaitType"] = DebugWaitType.IMMEDIATE, ) -> None: self.wait_word_list = wait_word_list self.wait_word_pos = wait_word_pos diff --git a/python/src/trezorlib/protobuf.py b/python/src/trezorlib/protobuf.py index 9109990e45..14f61dff87 100644 --- a/python/src/trezorlib/protobuf.py +++ b/python/src/trezorlib/protobuf.py @@ -546,12 +546,17 @@ def format_message( return printable / len(bytes) > 0.8 def pformat(name: str, value: t.Any, indent: int) -> str: + from . import messages + level = sep * indent leadin = sep * (indent + 1) if isinstance(value, MessageType): return format_message(value, indent, sep) + if isinstance(pb, messages.DebugLinkState) and name == "tokens": + return "".join(value) + if isinstance(value, list): # short list of simple values if not value or all(isinstance(x, int) for x in value): diff --git a/rust/trezor-client/src/protos/generated/messages_debug.rs b/rust/trezor-client/src/protos/generated/messages_debug.rs index e69ec5ec5f..3ced865e26 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 0ea232f5bf..364d6351dd 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 5a2adcbe44..211bed200e 100644 --- a/tests/device_tests/bitcoin/test_signmessage.py +++ b/tests/device_tests/bitcoin/test_signmessage.py @@ -383,8 +383,8 @@ def test_signmessage_pagination_trailing_newline(client: Client): [ # expect address confirmation message_filters.ButtonRequest(code=messages.ButtonRequestType.Other), - # expect a ButtonRequest that does not have pagination set - message_filters.ButtonRequest(pages=None), + # expect a ButtonRequest for a single-page screen + message_filters.ButtonRequest(pages=1), messages.MessageSignature, ] )