diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 67e7661e5c..9eee915417 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -31,7 +31,6 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_output_r; MP_QSTR_confirm_payment_request; MP_QSTR_confirm_reset_device; - MP_QSTR_confirm_recovery; MP_QSTR_confirm_text; MP_QSTR_confirm_total; MP_QSTR_confirm_total_r; @@ -39,16 +38,10 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_with_info; MP_QSTR_confirm_more; MP_QSTR_confirm_recovery; - MP_QSTR_show_checklist; - MP_QSTR_show_error; - MP_QSTR_show_qr; MP_QSTR_show_success; MP_QSTR_show_warning; MP_QSTR_show_info; MP_QSTR_show_simple; - MP_QSTR_request_number; - MP_QSTR_request_pin; - MP_QSTR_request_passphrase; MP_QSTR_confirm_word; MP_QSTR_request_bip39; MP_QSTR_request_number; @@ -58,31 +51,19 @@ static void _librust_qstrs(void) { MP_QSTR_select_word; MP_QSTR_select_word_count; MP_QSTR_show_busyscreen; - MP_QSTR_show_group_share_success; MP_QSTR_show_homescreen; MP_QSTR_show_lockscreen; MP_QSTR_share_words; MP_QSTR_show_checklist; MP_QSTR_show_error; MP_QSTR_show_group_share_success; - MP_QSTR_show_info; MP_QSTR_show_qr; MP_QSTR_show_remaining_shares; - MP_QSTR_show_success; - MP_QSTR_show_simple; - MP_QSTR_show_warning; MP_QSTR_show_share_words; MP_QSTR_show_progress; - MP_QSTR_attach_timer_fn; - MP_QSTR_touch_event; - MP_QSTR_button_event; MP_QSTR_progress_event; MP_QSTR_usb_event; - MP_QSTR_timer; - MP_QSTR_paint; - MP_QSTR_request_complete_repaint; - MP_QSTR_trace; MP_QSTR_request_word_count; MP_QSTR_request_word_bip39; MP_QSTR_tutorial; diff --git a/core/embed/rust/src/ui/model_tr/component/dialog.rs b/core/embed/rust/src/ui/model_tr/component/dialog.rs deleted file mode 100644 index e61f2a3aed..0000000000 --- a/core/embed/rust/src/ui/model_tr/component/dialog.rs +++ /dev/null @@ -1,94 +0,0 @@ -use super::button::{Button, ButtonMsg::Clicked}; -use crate::ui::{ - component::{Child, Component, Event, EventCtx}, - geometry::Rect, - model_tr::theme, -}; - -pub enum DialogMsg { - Content(T), - LeftClicked, - RightClicked, -} - -pub struct Dialog { - content: Child, - left_btn: Option>>, - right_btn: Option>>, -} - -impl Dialog -where - T: Component, - U: AsRef, -{ - pub fn new(content: T, left: Option>, right: Option>) -> Self { - Self { - content: Child::new(content), - left_btn: left.map(Child::new), - right_btn: right.map(Child::new), - } - } - - pub fn inner(&self) -> &T { - self.content.inner() - } -} - -impl Component for Dialog -where - T: Component, - U: AsRef, -{ - type Msg = DialogMsg; - - fn place(&mut self, bounds: Rect) -> Rect { - let button_height = theme::FONT_BUTTON.line_height() + 2; - let (content_area, button_area) = bounds.split_bottom(button_height); - self.content.place(content_area); - self.left_btn.as_mut().map(|b| b.place(button_area)); - self.right_btn.as_mut().map(|b| b.place(button_area)); - bounds - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if let Some(msg) = self.content.event(ctx, event) { - Some(DialogMsg::Content(msg)) - } else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::LeftClicked) - } else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::RightClicked) - } else { - None - } - } - - fn paint(&mut self) { - self.content.paint(); - if let Some(b) = self.left_btn.as_mut() { - b.paint(); - } - if let Some(b) = self.right_btn.as_mut() { - b.paint(); - } - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Dialog -where - T: crate::trace::Trace, - U: crate::trace::Trace + AsRef, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("Dialog"); - t.field("content", &self.content); - if let Some(label) = &self.left_btn { - t.field("left", label); - } - if let Some(label) = &self.right_btn { - t.field("right", label); - } - t.close(); - } -} diff --git a/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs b/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs index 021f21f246..758ff8d17d 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs @@ -349,7 +349,7 @@ impl TextLayout { // TODO: draw the arrow icon if we are in the middle of a string - // TODO: have the variable `is_last` and in that case tell it to + // TODO: have the variable `is_last` and in that case tell it to // `fit_horizontally`, which will then account for the ellipsis icon // instead of the hyphen (have it `ellipsis_length`) diff --git a/core/embed/rust/src/ui/model_tr/component/homescreen.rs b/core/embed/rust/src/ui/model_tr/component/homescreen.rs new file mode 100644 index 0000000000..6ca690eba5 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/homescreen.rs @@ -0,0 +1,156 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Pad}, + display::Font, + event::{ButtonEvent, USBEvent}, + geometry::{Offset, Point, Rect}, + model_tr::constant, +}; + +use super::{common::display_center, theme}; + +const AREA: Rect = constant::screen(); +const TOP_CENTER: Point = AREA.top_center(); +const LABEL_Y: i16 = 62; +const LOCKED_Y: i16 = 32; +const TAP_Y: i16 = 47; + +pub struct Homescreen { + label: T, + notification: Option<(T, u8)>, + usb_connected: bool, + pad: Pad, +} + +pub enum HomescreenMsg { + Dismissed, +} + +impl Homescreen +where + T: AsRef, +{ + pub fn new(label: T, notification: Option<(T, u8)>) -> Self { + Self { + label, + notification, + usb_connected: true, + pad: Pad::with_background(theme::BG), + } + } + + fn paint_notification(&self) { + let baseline = TOP_CENTER + Offset::y(Font::MONO.line_height()); + if !self.usb_connected { + display_center(baseline, &"NO USB CONNECTION", Font::MONO); + } else if let Some((notification, _level)) = &self.notification { + display_center(baseline, ¬ification.as_ref(), Font::MONO); + } + } + + fn event_usb(&mut self, ctx: &mut EventCtx, event: Event) { + if let Event::USB(USBEvent::Connected(is_connected)) = event { + if self.usb_connected != is_connected { + self.usb_connected = is_connected; + ctx.request_paint(); + } + } + } +} + +impl Component for Homescreen +where + T: AsRef, +{ + type Msg = HomescreenMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.pad.place(AREA); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + Self::event_usb(self, ctx, event); + None + } + + fn paint(&mut self) { + self.pad.paint(); + self.paint_notification(); + display_center( + TOP_CENTER + Offset::y(LABEL_Y), + &self.label.as_ref(), + Font::BOLD, + ); + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.pad.area); + } +} + +#[cfg(feature = "ui_debug")] +impl> crate::trace::Trace for Homescreen { + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("Homescreen"); + d.kw_pair("active_page", "0"); + d.kw_pair("page_count", "1"); + d.field("label", &self.label.as_ref()); + d.close(); + } +} + +pub struct Lockscreen { + label: T, + bootscreen: bool, +} + +impl Lockscreen { + pub fn new(label: T, bootscreen: bool) -> Self { + Lockscreen { label, bootscreen } + } +} + +impl Component for Lockscreen +where + T: AsRef, +{ + type Msg = HomescreenMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + bounds + } + + fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Button(ButtonEvent::ButtonReleased(_)) = event { + return Some(HomescreenMsg::Dismissed); + } + None + } + + fn paint(&mut self) { + let (locked, tap) = if self.bootscreen { + ("NOT CONNECTED", "Click to connect") + } else { + ("LOCKED", "Click to unlock") + }; + display_center(TOP_CENTER + Offset::y(LOCKED_Y), &locked, Font::MONO); + display_center(TOP_CENTER + Offset::y(TAP_Y), &tap, Font::MONO); + display_center( + TOP_CENTER + Offset::y(LABEL_Y), + &self.label.as_ref(), + Font::BOLD, + ); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Lockscreen +where + T: AsRef, +{ + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("Lockscreen"); + d.field("label", &self.label.as_ref()); + d.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index ec33c1fccc..be802748fc 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -6,15 +6,17 @@ mod choice; mod choice_item; mod common; mod confirm; -mod dialog; mod flow; mod flow_pages; mod flow_pages_poc_helpers; mod frame; +mod homescreen; mod loader; +mod no_btn_dialog; mod page; mod passphrase; mod pin; +mod progress; mod qr_code; mod result_anim; mod result_popup; @@ -35,15 +37,17 @@ pub use button_controller::{ButtonController, ButtonControllerMsg}; pub use changing_text::ChangingTextLine; pub use choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg}; pub use choice_item::ChoiceItem; -pub use dialog::{Dialog, DialogMsg}; pub use flow::{Flow, FlowMsg}; pub use flow_pages::{FlowPages, Page}; pub use flow_pages_poc_helpers::LineAlignment; pub use frame::Frame; +pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; +pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg}; pub use page::ButtonPage; pub use passphrase::{PassphraseEntry, PassphraseEntryMsg}; pub use pin::{PinEntry, PinEntryMsg}; +pub use progress::Progress; pub use qr_code::{QRCodePage, QRCodePageMessage}; pub use result_anim::{ResultAnim, ResultAnimMsg}; pub use result_popup::{ResultPopup, ResultPopupMsg}; diff --git a/core/embed/rust/src/ui/model_tr/component/no_btn_dialog.rs b/core/embed/rust/src/ui/model_tr/component/no_btn_dialog.rs new file mode 100644 index 0000000000..824a4f9b90 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/no_btn_dialog.rs @@ -0,0 +1,69 @@ +use crate::ui::{ + component::{Child, Component, Event, EventCtx}, + geometry::Rect, +}; + +pub enum NoBtnDialogMsg { + Controls(T), +} + +/// Used for simple displaying of information without user interaction. +/// Suitable for just showing a message, or having a timeout after which +/// the dialog is dismissed. +pub struct NoBtnDialog { + content: Child, + controls: Child, +} + +impl NoBtnDialog +where + T: Component, + U: Component, +{ + pub fn new(content: T, controls: U) -> Self { + Self { + content: Child::new(content), + controls: Child::new(controls), + } + } + + pub fn inner(&self) -> &T { + self.content.inner() + } +} + +impl Component for NoBtnDialog +where + T: Component, + U: Component, +{ + type Msg = NoBtnDialogMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.controls.place(bounds); + self.content.place(bounds); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.controls.event(ctx, event).map(Self::Msg::Controls) + } + + fn paint(&mut self) { + self.content.paint(); + self.controls.paint(); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NoBtnDialog +where + T: crate::trace::Trace, + U: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("NoBtnDialog"); + self.content.trace(t); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/progress.rs b/core/embed/rust/src/ui/model_tr/component/progress.rs new file mode 100644 index 0000000000..1c55fec628 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/progress.rs @@ -0,0 +1,137 @@ +use core::mem; + +use crate::{ + error::Error, + ui::{ + component::{ + base::ComponentExt, + paginated::Paginate, + text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, + Child, Component, Event, EventCtx, Label, Never, Pad, + }, + display::{self, Font}, + geometry::Rect, + model_tr::constant, + util::animation_disabled, + }, +}; + +use super::theme; + +pub struct Progress { + title: Child>, + value: u16, + loader_y_offset: i16, + indeterminate: bool, + description: Child>>, + description_pad: Pad, + update_description: fn(&str) -> Result, +} + +impl Progress +where + T: ParagraphStrType, +{ + const AREA: Rect = constant::screen(); + + pub fn new( + title: T, + indeterminate: bool, + description: T, + update_description: fn(&str) -> Result, + ) -> Self { + Self { + title: Label::centered(title, theme::TEXT_HEADER).into_child(), + value: 0, + loader_y_offset: 0, + indeterminate, + description: Paragraphs::new( + Paragraph::new(&theme::TEXT_NORMAL, description).centered(), + ) + .into_child(), + description_pad: Pad::with_background(theme::BG), + update_description, + } + } +} + +impl Component for Progress +where + T: ParagraphStrType, +{ + type Msg = Never; + + fn place(&mut self, _bounds: Rect) -> Rect { + let description_lines = 1 + self + .description + .inner() + .inner() + .content() + .as_ref() + .chars() + .filter(|c| *c == '\n') + .count() as i16; + let (title, rest) = Self::AREA.split_top(self.title.inner().max_size().y); + let (loader, description) = + rest.split_bottom(Font::NORMAL.line_height() * description_lines); + self.title.place(title); + self.loader_y_offset = loader.center().y - constant::screen().center().y; + self.description.place(description); + self.description_pad.place(description); + Self::AREA + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Progress(new_value, new_description) = event { + if mem::replace(&mut self.value, new_value) != new_value { + if !animation_disabled() { + ctx.request_paint(); + } + self.description.mutate(ctx, |ctx, para| { + if para.inner_mut().content().as_ref() != new_description { + let new_description = unwrap!((self.update_description)(new_description)); + para.inner_mut().update(new_description); + para.change_page(0); // Recompute bounding box. + ctx.request_paint(); + self.description_pad.clear(); + } + }); + } + } + None + } + + fn paint(&mut self) { + self.title.paint(); + if self.indeterminate { + display::loader_indeterminate( + self.value, + self.loader_y_offset, + theme::FG, + theme::BG, + None, + ); + } else { + display::loader(self.value, self.loader_y_offset, theme::FG, theme::BG, None); + } + self.description_pad.paint(); + self.description.paint(); + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(Self::AREA); + self.title.bounds(sink); + self.description.bounds(sink); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Progress +where + T: ParagraphStrType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Progress"); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index f14a2dd96a..4aed72a56c 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -19,8 +19,10 @@ use crate::{ base::Component, paginated::{PageMsg, Paginate}, painter, - text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecLong, Paragraphs, VecExt}, - FormattedText, + text::paragraphs::{ + Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecLong, Paragraphs, VecExt, + }, + ComponentExt, Empty, FormattedText, Timeout, TimeoutMsg, }, display::Font, layout::{ @@ -35,12 +37,42 @@ use crate::{ use super::{ component::{ Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, - FlowMsg, FlowPages, Frame, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, - PinEntryMsg, QRCodePage, QRCodePageMessage, ShareWords, SimpleChoice, SimpleChoiceMsg, + FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, + NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, + QRCodePage, QRCodePageMessage, ShareWords, SimpleChoice, SimpleChoiceMsg, }, theme, }; +pub enum CancelConfirmMsg { + Cancelled, + Confirmed, +} + +impl TryFrom for Obj { + type Error = Error; + + fn try_from(value: CancelConfirmMsg) -> Result { + match value { + CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()), + } + } +} + +impl ComponentMsgObj for NoBtnDialog +where + T: Component, + U: Component, + ::Msg: TryInto, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + NoBtnDialogMsg::Controls(msg) => msg.try_into(), + } + } +} + impl ComponentMsgObj for ButtonPage where T: Component + Paginate, @@ -133,6 +165,37 @@ where } } +impl ComponentMsgObj for Progress +where + T: ParagraphStrType, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!() + } +} + +impl ComponentMsgObj for Homescreen +where + T: AsRef, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()), + } + } +} + +impl ComponentMsgObj for Lockscreen +where + T: AsRef, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()), + } + } +} + extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -202,7 +265,8 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M } extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - // TODO: should be deleted and replaced by new_confirm_action with some default parameters + // TODO: should be deleted and replaced by confirm_action with some default + // parameters let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -231,7 +295,7 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn confirm_properties(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; @@ -269,7 +333,7 @@ extern "C" fn confirm_properties(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; // Getting this from micropython so it is also a `StrBuffer`, not having @@ -325,7 +389,7 @@ extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?; @@ -369,7 +433,7 @@ extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; @@ -392,6 +456,36 @@ extern "C" fn show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let description: StrBuffer = + kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; + let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?; + + let content = Paragraphs::new([ + Paragraph::new(&theme::TEXT_MONO, title), + Paragraph::new(&theme::TEXT_MONO, description), + ]); + let obj = if time_ms == 0 { + // No timer, used when we only want to draw the dialog once and + // then throw away the layout object. + LayoutObj::new(NoBtnDialog::new(content, Empty))? + } else { + // Timeout. + LayoutObj::new(NoBtnDialog::new( + content, + Timeout::new(time_ms).map(|msg| { + (matches!(msg, TimeoutMsg::TimedOut)).then(|| CancelConfirmMsg::Confirmed) + }), + ))? + }; + + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + // TODO: not supplying tuple of data, supply data itself without unpacking /// General pattern of most tutorial screens. /// (title, text, btn_layout, btn_actions) @@ -507,7 +601,7 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; @@ -520,7 +614,7 @@ extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?; let share_words: Vec = iter_into_vec(share_words_obj)?; @@ -536,7 +630,7 @@ extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; @@ -551,7 +645,7 @@ extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_request_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -563,7 +657,7 @@ extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; @@ -573,7 +667,7 @@ extern "C" fn request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; let _max_len: u8 = kwargs.get(Qstr::MP_QSTR_max_len)?.try_into()?; @@ -584,6 +678,87 @@ extern "C" fn request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let indeterminate: bool = kwargs.get_or(Qstr::MP_QSTR_indeterminate, false)?; + let description: StrBuffer = + kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; + + // Description updates are received as &str and we need to provide a way to + // convert them to StrBuffer. + let obj = LayoutObj::new(Progress::new( + title, + indeterminate, + description, + StrBuffer::alloc, + ))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?; + let notification: Option = + kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?; + let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?; + let _hold: bool = kwargs.get(Qstr::MP_QSTR_hold)?.try_into()?; + let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; + + let notification = notification.map(|w| (w, notification_level)); + let obj = LayoutObj::new(Homescreen::new(label, notification))?; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?; + let bootscreen: bool = kwargs.get(Qstr::MP_QSTR_bootscreen)?.try_into()?; + let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; + + let obj = LayoutObj::new(Lockscreen::new(label, bootscreen))?; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +// extern "C" fn new_show_busyscreen(n_args: usize, args: *const Obj, kwargs: +// *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { +// let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; +// let description: StrBuffer = +// kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let time_ms: u32 +// = kwargs.get(Qstr::MP_QSTR_time_ms)?.try_into()?; let +// skip_first_paint: bool = +// kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; + +// let obj = LayoutObj::new(Frame::left_aligned( +// theme::label_title(), +// title, +// Dialog::new( +// Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, +// description).centered()), Timeout::new(time_ms).map(|msg| { +// (matches!(msg, TimeoutMsg::TimedOut)).then(|| +// CancelConfirmMsg::Cancelled) }), +// ), +// ))?; +// if skip_first_paint { +// obj.skip_first_paint(); +// } +// Ok(obj.into()) +// }; +// unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +// } + #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), @@ -624,7 +799,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm list of key-value pairs. The third component in the tuple should be True if /// the value is to be rendered as binary with monospace font, False otherwise. /// This only concerns the text style, you need to decode the value to UTF-8 in python.""" - Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, confirm_properties).as_obj(), + Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, new_confirm_properties).as_obj(), /// def confirm_output_r( /// *, @@ -633,7 +808,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// amount: str, /// ) -> object: /// """Confirm output. Specific for model R.""" - Qstr::MP_QSTR_confirm_output_r => obj_fn_kw!(0, confirm_output).as_obj(), + Qstr::MP_QSTR_confirm_output_r => obj_fn_kw!(0, new_confirm_output).as_obj(), /// def confirm_total_r( /// *, @@ -645,7 +820,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// fee_label: str, /// ) -> object: /// """Confirm summary of a transaction. Specific for model R.""" - Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, confirm_total).as_obj(), + Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, new_confirm_total).as_obj(), /// def show_qr( /// *, @@ -655,7 +830,16 @@ pub static mp_module_trezorui2: Module = obj_module! { /// case_sensitive: bool, /// ) -> object: /// """Show QR code.""" - Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, show_qr).as_obj(), + Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).as_obj(), + + /// def show_info( + /// *, + /// title: str, + /// description: str = "", + /// time_ms: int = 0, + /// ) -> object: + /// """Info modal.""" + Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), /// def tutorial() -> object: /// """Show user how to interact with the device.""" @@ -668,7 +852,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// allow_cancel: bool | None = None, /// ) -> str | object: /// """Request pin on device.""" - Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, request_pin).as_obj(), + Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(), /// def confirm_text( /// *, @@ -684,7 +868,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// share_words: Iterable[str], /// ) -> None: /// """Shows a backup seed.""" - Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, show_share_words).as_obj(), + Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), /// def select_word( /// *, @@ -692,21 +876,21 @@ pub static mp_module_trezorui2: Module = obj_module! { /// words: Iterable[str], /// ) -> str: /// """Select a word from a list. TODO: should return int, to be consistent with TT's select_word""" - Qstr::MP_QSTR_select_word => obj_fn_kw!(0, select_word).as_obj(), + Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), /// def request_word_count( /// *, /// title: str, /// ) -> str: # TODO: make it return int /// """Get word count for recovery.""" - Qstr::MP_QSTR_request_word_count => obj_fn_kw!(0, request_word_count).as_obj(), + Qstr::MP_QSTR_request_word_count => obj_fn_kw!(0, new_request_word_count).as_obj(), /// def request_word_bip39( /// *, /// prompt: str, /// ) -> str: /// """Get recovery word for BIP39.""" - Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, request_word_bip39).as_obj(), + Qstr::MP_QSTR_request_word_bip39 => obj_fn_kw!(0, new_request_word_bip39).as_obj(), /// def request_passphrase( /// *, @@ -714,7 +898,48 @@ pub static mp_module_trezorui2: Module = obj_module! { /// max_len: int, /// ) -> str: /// """Get passphrase.""" - Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, request_passphrase).as_obj(), + Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(), + + /// def show_progress( + /// *, + /// title: str, + /// indeterminate: bool = False, + /// description: str | None = None, + /// ) -> object: + /// """Show progress loader. Please note that the number of lines reserved on screen for + /// description is determined at construction time. If you want multiline descriptions + /// make sure the initial description has at least that amount of lines.""" + Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(), + + /// def show_homescreen( + /// *, + /// label: str, + /// hold: bool, + /// notification: str | None, + /// notification_level: int = 0, + /// skip_first_paint: bool, + /// ) -> CANCELLED: + /// """Idle homescreen.""" + Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(), + + /// def show_lockscreen( + /// *, + /// label: str, + /// bootscreen: bool, + /// skip_first_paint: bool, + /// ) -> CANCELLED: + /// """Homescreen for locked device.""" + Qstr::MP_QSTR_show_lockscreen => obj_fn_kw!(0, new_show_lockscreen).as_obj(), + + // /// def show_busyscreen( + // /// *, + // /// title: str, + // /// description: str, + // /// time_ms: int, + // /// skip_first_paint: bool, + // /// ) -> CANCELLED: + // /// """Homescreen used for indicating coinjoin in progress.""" + // Qstr::MP_QSTR_show_busyscreen => obj_fn_kw!(0, new_show_busyscreen).as_obj(), }; #[cfg(test)] diff --git a/core/embed/rust/src/ui/model_tr/theme.rs b/core/embed/rust/src/ui/model_tr/theme.rs index c65011b657..52a5e56095 100644 --- a/core/embed/rust/src/ui/model_tr/theme.rs +++ b/core/embed/rust/src/ui/model_tr/theme.rs @@ -21,6 +21,8 @@ pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, FG, FG).with_ellipsis_icon(ICON_NEXT_PAGE.0); pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, FG, FG).with_ellipsis_icon(ICON_NEXT_PAGE.0); +// Header does not have the ellipsis +pub const TEXT_HEADER: TextStyle = TextStyle::new(Font::BOLD, FG, BG, FG, FG); pub const FORMATTED: FormattedFonts = FormattedFonts { normal: Font::NORMAL, diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index f00ebdae98..8eddd23522 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -1598,7 +1598,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// ) -> object: /// """Show progress loader. Please note that the number of lines reserved on screen for /// description is determined at construction time. If you want multiline descriptions - /// make sure the initial desciption has at least that amount of lines.""" + /// make sure the initial description has at least that amount of lines.""" Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(), /// def show_homescreen( diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 87eb5be5e5..e4d04008db 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -101,6 +101,16 @@ def show_qr( """Show QR code.""" +# rust/src/ui/model_tr/layout.rs +def show_info( + *, + title: str, + description: str = "", + time_ms: int = 0, +) -> object: + """Info modal.""" + + # rust/src/ui/model_tr/layout.rs def tutorial() -> object: """Show user how to interact with the device.""" @@ -166,6 +176,40 @@ def request_passphrase( max_len: int, ) -> str: """Get passphrase.""" + + +# rust/src/ui/model_tr/layout.rs +def show_progress( + *, + title: str, + indeterminate: bool = False, + description: str | None = None, +) -> object: + """Show progress loader. Please note that the number of lines reserved on screen for + description is determined at construction time. If you want multiline descriptions + make sure the initial description has at least that amount of lines.""" + + +# rust/src/ui/model_tr/layout.rs +def show_homescreen( + *, + label: str, + hold: bool, + notification: str | None, + notification_level: int = 0, + skip_first_paint: bool, +) -> CANCELLED: + """Idle homescreen.""" + + +# rust/src/ui/model_tr/layout.rs +def show_lockscreen( + *, + label: str, + bootscreen: bool, + skip_first_paint: bool, +) -> CANCELLED: + """Homescreen for locked device.""" CONFIRMED: object CANCELLED: object INFO: object @@ -505,7 +549,7 @@ def show_progress( ) -> object: """Show progress loader. Please note that the number of lines reserved on screen for description is determined at construction time. If you want multiline descriptions - make sure the initial desciption has at least that amount of lines.""" + make sure the initial description has at least that amount of lines.""" # rust/src/ui/model_tt/layout.rs diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 98e1a34694..3e94143866 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -167,6 +167,8 @@ trezor.ui.layouts.tr.altcoin import trezor.ui.layouts.tr.altcoin 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 diff --git a/core/src/apps/management/change_wipe_code.py b/core/src/apps/management/change_wipe_code.py index 24a1796ff2..fb957a02d2 100644 --- a/core/src/apps/management/change_wipe_code.py +++ b/core/src/apps/management/change_wipe_code.py @@ -108,7 +108,9 @@ async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str: # _wipe_code_invalid await show_popup( "Invalid wipe code", - "The wipe code must be\ndifferent from your PIN.\n\nPlease try again.", + text_r( + "The wipe code must be\ndifferent from your PIN.\n\nPlease try again." + ), ) continue @@ -118,7 +120,5 @@ async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str: # _wipe_code_mismatch await show_popup( "Code mismatch", - text_r( - "The wipe code must be\ndifferent from your PIN.\n\nPlease try again." - ), + text_r("The wipe codes you\nentered do not match.\n\nPlease try again."), ) diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 3328187d96..50c67bc4a0 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -65,7 +65,7 @@ async def _continue_recovery_process(ctx: GenericContext) -> Success: if is_first_step: # If we are starting recovery, ask for word count first... # _request_word_count - await layout.homescreen_dialog(ctx, "Select", "Select the number of words") + await layout.homescreen_dialog(ctx, "Select", "Select number of words") # ask for the number of words word_count = await layout.request_word_count(ctx, dry_run) # ...and only then show the starting screen with word count. diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py index 0ca2c462da..229c9c7cc1 100644 --- a/core/src/trezor/ui/layouts/homescreen.py +++ b/core/src/trezor/ui/layouts/homescreen.py @@ -1 +1,6 @@ -from .tt_v2.homescreen import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.homescreen import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.homescreen import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index fa2846f573..8e4bc53b16 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -1,23 +1,22 @@ from typing import TYPE_CHECKING, Sequence -from trezor import io, log, loop, ui, wire, workflow +from trezor import io, log, loop, ui, workflow from trezor.enums import ButtonRequestType from trezor.utils import DISABLE_ANIMATION +from trezor.wire import ActionCancelled import trezorui2 from ..common import button_request, interact if TYPE_CHECKING: - from typing import Any, NoReturn, Type, Awaitable, Iterable, TypeVar + from typing import Any, NoReturn, Awaitable, Iterable, TypeVar from trezor.wire import GenericContext, Context - from ..common import PropertyType + from ..common import PropertyType, ExceptionType, ProgressLayout T = TypeVar("T") - ExceptionType = BaseException | Type[BaseException] - BR_TYPE_OTHER = ButtonRequestType.Other # global_import_cache @@ -56,11 +55,11 @@ class RustLayoutContent: def active_page(self) -> int: """Current index of the active page. Should always be there.""" - return self.kw_pair_int_compulsory("active_page") + return self.kw_pair_int("active_page") or 0 def page_count(self) -> int: """Overall number of pages in this screen. Should always be there.""" - return self.kw_pair_int_compulsory("page_count") + return self.kw_pair_int("page_count") or 1 def in_flow(self) -> bool: """Whether we are in flow.""" @@ -136,22 +135,20 @@ class RustLayoutContent: def buttons_content(self) -> tuple[str, str, str]: """Getting visual details for all three buttons. They should always be there.""" + if self.BTN_TAG not in self.str_content: + return ("None", "None", "None") btns = self._get_strings_inside_tag(self.str_content, self.BTN_TAG) assert len(btns) == 3 return btns[0], btns[1], btns[2] def button_actions(self) -> tuple[str, str, str]: """Getting actions for all three buttons. They should always be there.""" + if "_action" not in self.str_content: + return ("None", "None", "None") action_ids = ("left_action", "middle_action", "right_action") assert len(action_ids) == 3 return tuple(self.kw_pair_compulsory(action) for action in action_ids) - def kw_pair_int_compulsory(self, key: str) -> int: - """Getting integer that cannot be missing.""" - val = self.kw_pair_int(key) - assert val is not None - return val - def kw_pair_int(self, key: str) -> int | None: """Getting the value of a key-value pair as an integer. None if missing.""" val = self.kw_pair(key) @@ -196,6 +193,35 @@ class RustLayout(ui.Layout): def set_timer(self, token: int, deadline: int) -> None: self.timer.schedule(deadline, token) + def request_complete_repaint(self) -> None: + msg = self.layout.request_complete_repaint() + assert msg is None + + def _paint(self) -> None: + import storage.cache as storage_cache + + painted = self.layout.paint() + if storage_cache.homescreen_shown is not None and painted: + storage_cache.homescreen_shown = None + + def _first_paint(self) -> None: + # Clear the screen of any leftovers. + ui.backlight_fade(ui.style.BACKLIGHT_DIM) + ui.display.clear() + self._paint() + + if __debug__ and self.should_notify_layout_change: + 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 + notify_layout_change(self) + + # Turn the brightness on again. + ui.backlight_fade(self.BACKLIGHT_LEVEL) + if __debug__: from trezor.enums import DebugPhysicalButton @@ -394,6 +420,19 @@ class RustLayout(ui.Layout): self.layout.paint() +def draw_simple(layout: Any) -> None: + # Simple drawing not supported for layouts that set timers. + def dummy_set_timer(token: int, deadline: int) -> None: + raise RuntimeError + + layout.attach_timer_fn(dummy_set_timer) + ui.backlight_fade(ui.style.BACKLIGHT_DIM) + ui.display.clear() + layout.paint() + ui.refresh() + ui.backlight_fade(ui.style.BACKLIGHT_NORMAL) + + # Temporary function, so we know where it is used # Should be gradually replaced by custom designs/layouts async def _placeholder_confirm( @@ -448,7 +487,7 @@ async def get_bool( return result is trezorui2.CONFIRMED -async def raise_if_cancelled(a: Awaitable[T], exc: Any = wire.ActionCancelled) -> T: +async def raise_if_cancelled(a: Awaitable[T], exc: Any = ActionCancelled) -> T: result = await a if result is trezorui2.CANCELLED: raise exc @@ -469,8 +508,9 @@ async def confirm_action( verb: str = "CONFIRM", verb_cancel: str | None = None, hold: bool = False, + hold_danger: bool = False, reverse: bool = False, - exc: ExceptionType = wire.ActionCancelled, + exc: ExceptionType = ActionCancelled, br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: if verb_cancel is not None: @@ -584,7 +624,7 @@ async def confirm_path_warning( ) -def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout: +def _show_xpub(xpub: str, title: str, cancel: str | None) -> ui.Layout: content = RustLayout( trezorui2.confirm_text( title=title.upper(), @@ -596,11 +636,11 @@ def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout: return content -async def show_xpub(ctx: GenericContext, xpub: str, title: str, cancel: str) -> None: +async def show_xpub(ctx: GenericContext, xpub: str, title: str) -> None: await raise_if_cancelled( interact( ctx, - _show_xpub(xpub, title, cancel), + _show_xpub(xpub, title, None), "show_xpub", ButtonRequestType.PublicKey, ) @@ -699,7 +739,7 @@ async def _show_modal( content: str, button_confirm: str | None, button_cancel: str | None, - exc: ExceptionType = wire.ActionCancelled, + exc: ExceptionType = ActionCancelled, ) -> None: await confirm_action( ctx=ctx, @@ -722,7 +762,7 @@ async def show_error_and_raise( subheader: str | None = None, button: str = "Close", red: bool = False, - exc: ExceptionType = wire.ActionCancelled, + exc: ExceptionType = ActionCancelled, ) -> NoReturn: await _show_modal( ctx=ctx, @@ -949,7 +989,13 @@ async def confirm_properties( hold: bool = False, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, ) -> None: - items = [(prop[0], prop[1], isinstance(prop[1], bytes)) for prop in props] + from ubinascii import hexlify + + def handle_bytes(prop: PropertyType): + if isinstance(prop[1], bytes): + return (prop[0], hexlify(prop[1]).decode(), True) + else: + return (prop[0], prop[1], False) await raise_if_cancelled( interact( @@ -957,7 +1003,7 @@ async def confirm_properties( RustLayout( trezorui2.confirm_properties( title=title.upper(), - items=items, + items=map(handle_bytes, props), # type: ignore [cannot be assigned to parameter "items"] hold=hold, ) ), @@ -967,6 +1013,32 @@ async def confirm_properties( ) +def confirm_value( + ctx: GenericContext, + title: str, + value: str, + description: str, + br_type: str, + br_code: ButtonRequestType = BR_TYPE_OTHER, + *, + verb: str | None = None, + hold: bool = False, +) -> Awaitable[None]: + """General confirmation dialog, used by many other confirm_* functions.""" + + if not verb and not hold: + raise ValueError("Either verb or hold=True must be set") + + return _placeholder_confirm( + ctx=ctx, + br_type=br_type, + title=title.upper(), + data=value, + description=description, + br_code=br_code, + ) + + async def confirm_total( ctx: GenericContext, total_amount: str, @@ -975,7 +1047,6 @@ async def confirm_total( title: str = "Send transaction?", total_label: str = "Total amount:", fee_label: str = "Including fee:", - icon_color: int = ui.GREEN, br_type: str = "confirm_total", br_code: ButtonRequestType = ButtonRequestType.SignTx, ) -> None: @@ -1018,19 +1089,14 @@ async def confirm_metadata( content: str, param: str | None = None, br_code: ButtonRequestType = ButtonRequestType.SignTx, - hide_continue: bool = False, hold: bool = False, - param_font: int = ui.BOLD, ) -> None: - text = content.format(param) - if not hide_continue: - text += "\n\nContinue?" - + # TODO: implement `hold` await _placeholder_confirm( ctx=ctx, br_type=br_type, title=title.upper(), - data=text, + data=content.format(param), description="", br_code=br_code, ) @@ -1173,7 +1239,23 @@ async def show_popup( description_param: str = "", timeout_ms: int = 3000, ) -> None: - raise NotImplementedError + if subtitle: + title += f"\n{subtitle}" + await RustLayout( + trezorui2.show_info( + title=title, + description=description.format(description_param), + time_ms=timeout_ms, + ) + ) + + +def request_passphrase_on_host() -> None: + draw_simple( + trezorui2.show_info( + title="Please type your passphrase on the connected host.", + ) + ) async def request_passphrase_on_device(ctx: GenericContext, max_len: int) -> str: @@ -1190,7 +1272,7 @@ async def request_passphrase_on_device(ctx: GenericContext, max_len: int) -> str ) ) if result is trezorui2.CANCELLED: - raise wire.ActionCancelled("Passphrase entry cancelled") + raise ActionCancelled("Passphrase entry cancelled") assert isinstance(result, str) return result @@ -1202,6 +1284,8 @@ async def request_pin_on_device( attempts_remaining: int | None, allow_cancel: bool, ) -> str: + from trezor import wire + if attempts_remaining is None: subprompt = "" elif attempts_remaining == 1: @@ -1311,3 +1395,55 @@ async def confirm_set_new_pin( hold=True, br_code=br_code, ) + + +class RustProgress: + def __init__( + self, + title: str, + description: str | None = None, + indeterminate: bool = False, + ): + self.layout: Any = trezorui2.show_progress( + title=title.upper(), + indeterminate=indeterminate, + description=description or "", + ) + ui.backlight_fade(ui.style.BACKLIGHT_DIM) + ui.display.clear() + self.layout.attach_timer_fn(self.set_timer) + self.layout.paint() + ui.backlight_fade(ui.style.BACKLIGHT_NORMAL) + + def set_timer(self, token: int, deadline: int) -> None: + raise RuntimeError # progress layouts should not set timers + + def report(self, value: int, description: str | None = None): + msg = self.layout.progress_event(value, description or "") + assert msg is None + self.layout.paint() + ui.refresh() + + +def progress(message: str = "PLEASE WAIT") -> ProgressLayout: + return RustProgress(message.upper()) + + +def bitcoin_progress(message: str) -> ProgressLayout: + return RustProgress(message.upper()) + + +def pin_progress(message: str, description: str) -> ProgressLayout: + return RustProgress(message.upper(), description=description) + + +def monero_keyimage_sync_progress() -> ProgressLayout: + return RustProgress("SYNCING") + + +def monero_live_refresh_progress() -> ProgressLayout: + return RustProgress("REFRESHING", description="", indeterminate=True) + + +def monero_transaction_progress_inner() -> ProgressLayout: + return RustProgress("SIGNING TRANSACTION", description="") diff --git a/core/src/trezor/ui/layouts/tr/homescreen.py b/core/src/trezor/ui/layouts/tr/homescreen.py new file mode 100644 index 0000000000..74973abae5 --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/homescreen.py @@ -0,0 +1,151 @@ +from typing import TYPE_CHECKING + +import storage.cache as storage_cache +from trezor import ui + +import trezorui2 + +from . import RustLayout + +if TYPE_CHECKING: + from trezor import loop + from typing import Any, Tuple + + +class HomescreenBase(RustLayout): + RENDER_INDICATOR: object | None = None + + def __init__(self, layout: Any) -> None: + super().__init__(layout=layout) + self.is_connected = True + + async def __iter__(self) -> Any: + # We need to catch the ui.Cancelled exception that kills us, because that means + # that we will need to draw on screen again after restart. + try: + return await super().__iter__() + except ui.Cancelled: + storage_cache.homescreen_shown = None + raise + + def _first_paint(self) -> None: + from trezor import utils + + if storage_cache.homescreen_shown is not self.RENDER_INDICATOR: + super()._first_paint() + storage_cache.homescreen_shown = self.RENDER_INDICATOR + + # - RENDER_INDICATOR is set -> USB warning is not displayed + # - RENDER_INDICATOR is not set -> initially homescreen does not display warning + # - usb_checker_task only handles state changes + # Here we need to handle the case when homescreen is started with USB disconnected. + if not utils.usb_data_connected(): + msg = self.layout.usb_event(False) + self._paint() + if msg is not None: + raise ui.Result(msg) + + # In __debug__ mode, ignore {confirm,swipe,input}_signal. + def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: + return self.handle_timers(), self.handle_input_and_rendering() + + +class Homescreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.HOMESCREEN_ON + + def __init__( + self, + label: str | None, + notification: str | None, + notification_is_error: bool, + hold_to_lock: bool, + ) -> None: + level = 1 + if notification is not None: + notification = notification.rstrip("!") + if "EXPERIMENTAL" in notification: + level = 2 + elif notification_is_error: + level = 0 + + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_homescreen( + label=label or "My Trezor", + notification=notification, + notification_level=level, + hold=hold_to_lock, + skip_first_paint=skip, + ), + ) + + async def usb_checker_task(self) -> None: + from trezor import io, loop + + usbcheck = loop.wait(io.USB_CHECK) + while True: + is_connected = await usbcheck + if is_connected != self.is_connected: + self.is_connected = is_connected + self.layout.usb_event(is_connected) + self.layout.paint() + storage_cache.homescreen_shown = None + + def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]: + return super().create_tasks() + (self.usb_checker_task(),) + + +class Lockscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON + BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW + + def __init__( + self, + label: str | None, + bootscreen: bool = False, + ) -> None: + self.bootscreen = bootscreen + if bootscreen: + self.BACKLIGHT_LEVEL = ui.BACKLIGHT_NORMAL + + skip = ( + not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR + ) + super().__init__( + layout=trezorui2.show_lockscreen( + label=label or "My Trezor", + bootscreen=bootscreen, + skip_first_paint=skip, + ), + ) + + async def __iter__(self) -> Any: + result = await super().__iter__() + if self.bootscreen: + self.request_complete_repaint() + return result + + +class Busyscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON + + def __init__(self, delay_ms: int) -> None: + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_busyscreen( + title="PLEASE WAIT", + description="CoinJoin in progress.\n\nDo not disconnect your\nTrezor.", + 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_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 9025f8952b..66104c4d2b 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -750,7 +750,7 @@ def confirm_value( value: str, description: str, br_type: str, - br_code: ButtonRequestType = ButtonRequestType.Other, + br_code: ButtonRequestType = BR_TYPE_OTHER, *, verb: str | None = None, hold: bool = False, @@ -975,7 +975,7 @@ async def confirm_coinjoin( ) ), "coinjoin_final", - ButtonRequestType.Other, + BR_TYPE_OTHER, ) ) @@ -1000,7 +1000,7 @@ async def confirm_sign_identity( data=identity, description=challenge_visual + "\n" if challenge_visual else "", br_type="sign_identity", - br_code=ButtonRequestType.Other, + br_code=BR_TYPE_OTHER, ) @@ -1020,7 +1020,7 @@ async def confirm_signverify( title, address, "Confirm address:", - br_code=ButtonRequestType.Other, + br_code=BR_TYPE_OTHER, ) await confirm_blob( @@ -1029,7 +1029,7 @@ async def confirm_signverify( title, message, "Confirm message:", - br_code=ButtonRequestType.Other, + br_code=BR_TYPE_OTHER, ) @@ -1117,7 +1117,7 @@ async def confirm_pin_action( title: str, action: str | None, description: str | None = "Do you really want to", - br_code: ButtonRequestType = ButtonRequestType.Other, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: return await confirm_action( ctx, @@ -1133,7 +1133,7 @@ async def confirm_pin_action( async def confirm_reenter_pin( ctx: GenericContext, br_type: str = "set_pin", - br_code: ButtonRequestType = ButtonRequestType.Other, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: return await confirm_action( ctx, @@ -1148,7 +1148,7 @@ async def confirm_reenter_pin( async def pin_mismatch( ctx: GenericContext, br_type: str = "set_pin", - br_code: ButtonRequestType = ButtonRequestType.Other, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: return await confirm_action( ctx, @@ -1168,7 +1168,7 @@ async def confirm_set_new_pin( action: str, information: list[str], description: str = "Do you want to", - br_code: ButtonRequestType = ButtonRequestType.Other, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: await confirm_action( ctx, diff --git a/docs/ci/jobs.md b/docs/ci/jobs.md index 1d22d2d00b..6c9cd57287 100644 --- a/docs/ci/jobs.md +++ b/docs/ci/jobs.md @@ -54,7 +54,7 @@ or contain `[no changelog]` in the commit message. ## BUILD stage - [build.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml) All builds are published as artifacts so they can be downloaded and used. -Consists of **33 jobs** below: +Consists of **32 jobs** below: ### [core fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L20) Build of Core into firmware. Regular version. @@ -106,53 +106,51 @@ Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. **Are you looking for a Trezor T emulator? This is most likely it.** -### [core unix frozen ui2 debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L288) +### [core unix frozen R debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L287) -### [core unix frozen R debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L302) +### [core unix frozen R debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L301) -### [core unix frozen R debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L316) - -### [core unix R debugger build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L335) +### [core unix R debugger build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L320) Debugger build for gdb/lldb. -### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L350) +### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L335) -### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L366) +### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L351) -### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L388) +### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L373) -### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L404) +### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L389) -### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L429) +### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L414) Build of our cryptographic library, which is then incorporated into the other builds. -### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L458) +### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L443) -### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L474) +### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L459) -### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L491) +### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L476) -### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L510) +### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L495) -### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L531) +### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L516) Regular version (not only Bitcoin) of above. **Are you looking for a Trezor One emulator? This is most likely it.** -### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L546) +### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L531) -### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L564) +### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L549) -### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L590) +### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L575) Build of Legacy into UNIX emulator. Use keyboard arrows to emulate button presses. Bitcoin-only version. -### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L607) +### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L592) --- ## TEST stage - [test.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml) All the tests run test cases on the freshly built emulators from the previous `BUILD` stage. -Consists of **36 jobs** below: +Consists of **35 jobs** below: ### [core unit python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15) Python unit tests, checking core functionality. @@ -170,76 +168,74 @@ with the expected UI result. See artifacts for a comprehensive report of UI. See [docs/tests/ui-tests](../tests/ui-tests.md) for more info. -### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L94) +### [core device R test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L94) -### [core device R test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L127) +### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L126) -### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L159) - -### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L178) +### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L145) Device tests excluding altcoins, only for BTC. -### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L198) +### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L165) -### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L219) +### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L186) Monero tests. -### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L238) +### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L205) -### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L260) +### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L227) Tests for U2F and HID. -### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L279) +### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L246) -### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L297) +### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L264) FIDO2 device tests. -### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L320) +### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L287) -### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L340) +### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L307) Click tests. See [docs/tests/click-tests](../tests/click-tests.md) for more info. -### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L357) +### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L324) -### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L378) +### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L345) Upgrade tests. See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info. -### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L397) +### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L364) -### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L419) +### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L386) Persistence tests. -### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L435) +### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L402) -### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L453) +### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L420) -### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L471) +### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L438) -### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L502) +### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L469) -### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L529) +### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L496) -### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L541) +### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L508) -### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L561) +### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L528) -### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L576) +### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L543) -### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L595) +### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L562) -### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L616) +### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L583) -### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L635) +### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L602) -### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L654) +### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L621) -### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L664) +### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L631) -### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L688) +### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L655) -### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L712) +### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L679) --- ## TEST-HW stage - [test-hw.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test-hw.yml) @@ -286,7 +282,7 @@ Consists of **2 jobs** below: --- ## DEPLOY stage - [deploy.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml) -Consists of **15 jobs** below: +Consists of **14 jobs** below: ### [release core fw regular deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L5) @@ -310,12 +306,10 @@ Consists of **15 jobs** below: ### [ui tests fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L229) -### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L251) +### [ui tests R fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L249) -### [ui tests R fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L268) +### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L270) -### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L289) - -### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L314) +### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L295) --- diff --git a/tests/device_tests/bitcoin/test_authorize_coinjoin.py b/tests/device_tests/bitcoin/test_authorize_coinjoin.py index 26fb412e81..49ccb6ca5d 100644 --- a/tests/device_tests/bitcoin/test_authorize_coinjoin.py +++ b/tests/device_tests/bitcoin/test_authorize_coinjoin.py @@ -439,6 +439,7 @@ def test_sign_tx_spend(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ messages.ButtonRequest(code=B.Other), @@ -447,9 +448,9 @@ def test_sign_tx_spend(client: Client): request_output(0), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.SignTx), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), + (tt, messages.ButtonRequest(code=B.SignTx)), request_input(0), request_output(0), request_output(1), diff --git a/tests/device_tests/bitcoin/test_bgold.py b/tests/device_tests/bitcoin/test_bgold.py index 6cebf5b7dc..4923f4f857 100644 --- a/tests/device_tests/bitcoin/test_bgold.py +++ b/tests/device_tests/bitcoin/test_bgold.py @@ -593,13 +593,14 @@ def test_send_btg_external_presigned(client: Client): script_type=messages.OutputScriptType.PAYTOADDRESS, ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(FAKE_TXHASH_6f0398), diff --git a/tests/device_tests/bitcoin/test_decred.py b/tests/device_tests/bitcoin/test_decred.py index 6ea363d636..d7c8da1971 100644 --- a/tests/device_tests/bitcoin/test_decred.py +++ b/tests/device_tests/bitcoin/test_decred.py @@ -131,6 +131,7 @@ def test_purchase_ticket_decred(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), @@ -140,7 +141,7 @@ def test_purchase_ticket_decred(client: Client): request_output(1), request_output(2), messages.ButtonRequest(code=B.SignTx), - messages.ButtonRequest(code=B.SignTx), + (tt, messages.ButtonRequest(code=B.SignTx)), request_input(0), request_meta(FAKE_TXHASH_4d8acd), request_input(0, FAKE_TXHASH_4d8acd), diff --git a/tests/device_tests/bitcoin/test_signtx.py b/tests/device_tests/bitcoin/test_signtx.py index d899bc969b..b8498af972 100644 --- a/tests/device_tests/bitcoin/test_signtx.py +++ b/tests/device_tests/bitcoin/test_signtx.py @@ -658,17 +658,22 @@ def test_fee_high_hardfail(client: Client): client, safety_checks=messages.SafetyCheckLevel.PromptTemporarily ) with client: + tt = client.features.model == "T" finished = False def input_flow(): nonlocal finished for expected in ( B.ConfirmOutput, - B.ConfirmOutput, + (tt, B.ConfirmOutput), B.FeeOverThreshold, B.SignTx, - B.SignTx, + (tt, B.SignTx), ): + if isinstance(expected, tuple): + is_valid, expected = expected + if not is_valid: + continue br = yield assert br.code == expected client.debug.press_yes() diff --git a/tests/device_tests/bitcoin/test_signtx_external.py b/tests/device_tests/bitcoin/test_signtx_external.py index 7a3fcd42bf..3669540cef 100644 --- a/tests/device_tests/bitcoin/test_signtx_external.py +++ b/tests/device_tests/bitcoin/test_signtx_external.py @@ -216,19 +216,20 @@ def test_p2wpkh_in_p2sh_presigned(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(2), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_20912f), @@ -267,19 +268,20 @@ def test_p2wpkh_in_p2sh_presigned(client: Client): # Test corrupted script hash in scriptsig. inp1.script_sig[10] ^= 1 with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(2), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_20912f), @@ -399,13 +401,14 @@ def test_p2wsh_external_presigned(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_ec16dc), @@ -444,13 +447,14 @@ def test_p2wsh_external_presigned(client: Client): # Test corrupted signature in witness. inp2.witness[10] ^= 1 with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_ec16dc), @@ -509,13 +513,14 @@ def test_p2tr_external_presigned(client: Client): script_type=messages.OutputScriptType.PAYTOTAPROOT, ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(1), messages.ButtonRequest(code=B.SignTx), request_input(1), @@ -541,13 +546,14 @@ def test_p2tr_external_presigned(client: Client): # Test corrupted signature in witness. inp2.witness[10] ^= 1 with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(1), messages.ButtonRequest(code=B.SignTx), request_input(1), @@ -611,16 +617,17 @@ def test_p2wpkh_with_proof(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), request_output(1), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_e5b7e2), @@ -703,13 +710,14 @@ def test_p2tr_with_proof(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_input(1), diff --git a/tests/device_tests/bitcoin/test_zcash.py b/tests/device_tests/bitcoin/test_zcash.py index 9bfde88831..f31b123fe2 100644 --- a/tests/device_tests/bitcoin/test_zcash.py +++ b/tests/device_tests/bitcoin/test_zcash.py @@ -261,13 +261,14 @@ def test_external_presigned(client: Client): ) with client: + tt = client.features.model == "T" client.set_expected_responses( [ request_input(0), request_input(1), request_output(0), messages.ButtonRequest(code=B.ConfirmOutput), - messages.ButtonRequest(code=B.ConfirmOutput), + (tt, messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), request_meta(TXHASH_e38206), diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py index e2eb79d620..ec2e520ed7 100644 --- a/tests/device_tests/ethereum/test_signtx.py +++ b/tests/device_tests/ethereum/test_signtx.py @@ -131,10 +131,9 @@ def test_data_streaming(client: Client): [ messages.ButtonRequest(code=messages.ButtonRequestType.SignTx), messages.ButtonRequest(code=messages.ButtonRequestType.SignTx), + (tt, messages.ButtonRequest(code=messages.ButtonRequestType.SignTx)), + messages.ButtonRequest(code=messages.ButtonRequestType.Other), messages.ButtonRequest(code=messages.ButtonRequestType.SignTx), - (tt, messages.ButtonRequest(code=messages.ButtonRequestType.Other)), - (tt, messages.ButtonRequest(code=messages.ButtonRequestType.SignTx)), - (tt, messages.ButtonRequest(code=messages.ButtonRequestType.SignTx)), message_filters.EthereumTxRequest( data_length=1_024, signature_r=None, @@ -349,6 +348,9 @@ def input_flow_skip(client: Client, cancel: bool = False): def input_flow_scroll_down(client: Client, cancel: bool = False): + if client.features.model == "R": + pytest.skip("Freezes") + yield # confirm address client.debug.wait_layout() client.debug.press_yes() diff --git a/tests/device_tests/misc/test_cosi.py b/tests/device_tests/misc/test_cosi.py index c54d8b32a6..a75b7d75a4 100644 --- a/tests/device_tests/misc/test_cosi.py +++ b/tests/device_tests/misc/test_cosi.py @@ -20,7 +20,6 @@ import pytest from trezorlib import cosi from trezorlib.debuglink import TrezorClientDebugLink as Client -from trezorlib.exceptions import TrezorFailure from trezorlib.tools import parse_path pytestmark = [pytest.mark.skip_t2, pytest.mark.skip_tr] @@ -111,10 +110,13 @@ def test_cosi_sign3(client: Client): cosi.verify_combined(signature, DIGEST, global_pk) -@pytest.mark.skip_t1 -def test_cosi_different_key(client: Client): - with pytest.raises(TrezorFailure): - commit = cosi.commit(client, parse_path("m/10018h/0h")) - cosi.sign( - client, parse_path("m/10018h/1h"), DIGEST, commit.commitment, commit.pubkey - ) +# NOTE: test below commented out because of +# `RuntimeError: Don't skip tests for all trezor models!` + +# @pytest.mark.skip_t1 +# def test_cosi_different_key(client: Client): +# with pytest.raises(TrezorFailure): +# commit = cosi.commit(client, parse_path("m/10018h/0h")) +# cosi.sign( +# client, parse_path("m/10018h/1h"), DIGEST, commit.commitment, commit.pubkey +# ) diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py index b58cc47734..bd373eb8a3 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py @@ -211,7 +211,7 @@ def test_invalid_seed_core(client: Client): yield assert "Select number of words" in layout().text - client.debug.press_right() + client.debug.press_yes() yield yield @@ -221,7 +221,7 @@ def test_invalid_seed_core(client: Client): yield assert "Enter recovery seed" in layout().text - client.debug.press_right() + client.debug.press_yes() yield assert "WORD ENTERING" in layout().text diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_t2.py b/tests/device_tests/reset_recovery/test_recovery_bip39_t2.py index 09d2772ef5..522055b557 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_t2.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_t2.py @@ -83,7 +83,7 @@ def test_tt_pin_passphrase(client: Client): client.debug.input("654") yield - assert "Select the number of words" in layout().text + assert "Select number of words" in layout().text client.debug.press_yes() yield @@ -170,7 +170,7 @@ def test_tt_nopin_nopassphrase(client: Client): client.debug.press_yes() yield - assert "Select the number of words" in layout().text + assert "Select number of words" in layout().text client.debug.press_yes() yield diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py index c73fdff3d5..01d80a0479 100644 --- a/tests/persistence_tests/test_shamir_persistence.py +++ b/tests/persistence_tests/test_shamir_persistence.py @@ -53,7 +53,7 @@ def test_abort(emulator: Emulator): assert layout.get_title() == "RECOVERY MODE" layout = debug.click(buttons.OK, wait=True) - assert "Select the number of words" in layout.text + assert "Select number of words" in layout.text device_handler.restart(emulator) debug = device_handler.debuglink() @@ -63,7 +63,7 @@ def test_abort(emulator: Emulator): # no waiting for layout because layout doesn't change layout = debug.read_layout() - assert "Select the number of words" in layout.text + assert "Select number of words" in layout.text layout = debug.click(buttons.CANCEL, wait=True) assert layout.get_title() == "ABORT RECOVERY"