From 10269d7a263be05656bb71ed186daee56ee60d17 Mon Sep 17 00:00:00 2001 From: obrusvit Date: Tue, 10 Dec 2024 17:10:05 +0100 Subject: [PATCH] feat(contacts): implement layout for TXOUT - SwipeFlow for TXOUT confirmation for labeled contact --- core/embed/rust/librust_qstr.h | 2 + .../flow/confirm_output_contact.rs | 134 ++++++++++++++++++ .../rust/src/ui/model_mercury/flow/mod.rs | 2 + .../rust/src/ui/model_mercury/flow/util.rs | 5 + .../embed/rust/src/ui/model_mercury/layout.rs | 47 ++++++ core/mocks/generated/trezorui2.pyi | 12 ++ core/src/apps/bitcoin/sign_tx/layout.py | 5 +- .../src/trezor/ui/layouts/mercury/__init__.py | 59 +++++--- 8 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 core/embed/rust/src/ui/model_mercury/flow/confirm_output_contact.rs diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index ef3215cd34..440dac4555 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -212,6 +212,7 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_total__title_sending_from; MP_QSTR_confirm_value; MP_QSTR_confirm_with_info; + MP_QSTR_contact_label; MP_QSTR_count; MP_QSTR_current; MP_QSTR_danger; @@ -248,6 +249,7 @@ static void _librust_qstrs(void) { MP_QSTR_firmware_update__title_fingerprint; MP_QSTR_first_screen; MP_QSTR_flow_confirm_output; + MP_QSTR_flow_confirm_output_contact; MP_QSTR_flow_confirm_reset; MP_QSTR_flow_confirm_set_new_pin; MP_QSTR_flow_continue_recovery; diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_output_contact.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_output_contact.rs new file mode 100644 index 0000000000..cb4ed99548 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_output_contact.rs @@ -0,0 +1,134 @@ +use crate::ui::button_request::{ButtonRequest, ButtonRequestCode}; +use crate::ui::component::text::paragraphs::ParagraphSource; +use crate::ui::component::ButtonRequestExt; +use crate::{ + error, + strutil::TString, + translations::TR, + ui::{ + component::{ + swipe_detect::SwipeSettings, text::paragraphs::ParagraphVecShort, ComponentExt, + }, + flow::{ + base::{Decision, DecisionBuilder}, + FlowController, FlowMsg, SwipeFlow, + }, + geometry::Direction, + model_mercury::{ + component::{ + Frame, FrameMsg, PromptMsg, PromptScreen, SwipeContent, VerticalMenu, + VerticalMenuChoiceMsg, + }, + theme, + }, + }, +}; + +use super::{ConfirmBlobParams, ShowInfoParams}; + +#[derive(Copy, Clone, PartialEq, Eq)] +enum ConfirmOutputContact { + Contact, + Amount, + Menu, + AddressInfo, + Confirm, +} +impl FlowController for ConfirmOutputContact { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, direction: Direction) -> Decision { + match (self, direction) { + (Self::Contact | Self::Amount, Direction::Left) => Self::Menu.swipe(direction), + (Self::Contact, Direction::Up) => Self::Amount.swipe(direction), + + (Self::Menu, Direction::Right) => Self::Contact.swipe(direction), + (Self::AddressInfo, Direction::Right) => Self::Menu.swipe(direction), + + (Self::Amount, Direction::Down) => Self::Contact.swipe(direction), + (Self::Amount, Direction::Up) => self.return_msg(FlowMsg::Confirmed), + + _ => self.do_nothing(), + } + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Contact, FlowMsg::Info) => Self::Menu.goto(), + (Self::Amount, FlowMsg::Info) => Self::Menu.goto(), + (Self::Menu, FlowMsg::Choice(0)) => Self::AddressInfo.goto(), + (Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Cancelled), + (Self::Menu, FlowMsg::Cancelled) => Self::Contact.swipe_right(), + (Self::AddressInfo, FlowMsg::Cancelled) => Self::Menu.goto(), + _ => self.do_nothing(), + } + } +} +pub fn new_confirm_output_contact( + paragraphs: ParagraphVecShort<'static>, + title: TString<'static>, + address_params: ShowInfoParams, + amount_params: ConfirmBlobParams, +) -> Result { + let paragraphs = paragraphs.into_paragraphs(); + let br_code = ButtonRequestCode::ConfirmOutput as u16; + let br_name = "confirm_output".into(); + + // Contact + let content_contact = Frame::left_aligned(title, SwipeContent::new(paragraphs)) + .with_swipe(Direction::Up, SwipeSettings::default()) + .with_swipe(Direction::Left, SwipeSettings::default()) + .with_menu_button() + .with_footer(TR::instructions__swipe_up.into(), None) + .map(move |msg| match msg { + FrameMsg::Button(_) => Some(FlowMsg::Info), + FrameMsg::Content(_) => Some(FlowMsg::Confirmed), + }) + .one_button_request(ButtonRequest::from_num(br_code, br_name)); + + // Amount + let content_amount = amount_params + .into_layout()? + .one_button_request(ButtonRequest::from_num(br_code, br_name)); + + // Address info + let content_address = address_params.into_layout()?; + + // Menu + let content_menu = VerticalMenu::empty() + .item(theme::ICON_CHEVRON_RIGHT, TR::words__address.into()) + .danger(theme::ICON_CANCEL, TR::send__cancel_sign.into()); + let content_menu = Frame::left_aligned(TString::empty(), content_menu) + .with_cancel_button() + .with_swipe(Direction::Right, SwipeSettings::immediate()) + .map(move |msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + }); + + // Hold to confirm + let content_confirm = Frame::left_aligned( + TR::send__sign_transaction.into(), + SwipeContent::new(PromptScreen::new_hold_to_confirm()), + ) + .with_menu_button() + .with_footer(TR::instructions__hold_to_sign.into(), None) + .with_swipe(Direction::Down, SwipeSettings::default()) + .with_swipe(Direction::Left, SwipeSettings::default()) + .map(|msg| match msg { + FrameMsg::Content(PromptMsg::Confirmed) => Some(FlowMsg::Confirmed), + FrameMsg::Button(_) => Some(FlowMsg::Info), + _ => None, + }); + + let res = SwipeFlow::new(&ConfirmOutputContact::Contact)? + .with_page(&ConfirmOutputContact::Contact, content_contact)? + .with_page(&ConfirmOutputContact::Amount, content_amount)? + .with_page(&ConfirmOutputContact::Menu, content_menu)? + .with_page(&ConfirmOutputContact::AddressInfo, content_address)? + .with_page(&ConfirmOutputContact::Confirm, content_confirm)?; + Ok(res) +} diff --git a/core/embed/rust/src/ui/model_mercury/flow/mod.rs b/core/embed/rust/src/ui/model_mercury/flow/mod.rs index cbfc7cfb08..c84237ec13 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -3,6 +3,7 @@ pub mod confirm_action; pub mod confirm_fido; pub mod confirm_firmware_update; pub mod confirm_output; +pub mod confirm_output_contact; pub mod confirm_reset; pub mod confirm_set_new_pin; pub mod confirm_summary; @@ -25,6 +26,7 @@ pub use confirm_action::{ pub use confirm_fido::new_confirm_fido; pub use confirm_firmware_update::new_confirm_firmware_update; pub use confirm_output::new_confirm_output; +pub use confirm_output_contact::new_confirm_output_contact; pub use confirm_reset::new_confirm_reset; pub use confirm_set_new_pin::SetNewPin; pub use confirm_summary::new_confirm_summary; diff --git a/core/embed/rust/src/ui/model_mercury/flow/util.rs b/core/embed/rust/src/ui/model_mercury/flow/util.rs index bef55ab099..c08fc55f44 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/util.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/util.rs @@ -343,6 +343,11 @@ impl ShowInfoParams { self.items.is_empty() } + pub const fn with_chunkify(mut self, chunkify: bool) -> Self { + self.chunkify = chunkify; + self + } + #[inline(never)] pub const fn with_subtitle(mut self, subtitle: Option>) -> Self { self.subtitle = subtitle; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 931c1c80bd..8a1d71aea7 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -622,6 +622,42 @@ extern "C" fn new_confirm_output(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_confirm_output_contact(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let contact_label: TString = kwargs.get(Qstr::MP_QSTR_contact_label)?.try_into()?; + let address: TString = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; + let amount: Obj = kwargs.get(Qstr::MP_QSTR_amount)?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, true)?; + + let paragraphs = ParagraphVecShort::from_iter([ + Paragraph::new(&theme::TEXT_SUB_GREY, "Contact"), + Paragraph::new(&theme::TEXT_SUPER, contact_label), + ]); + + let amount_params = ConfirmBlobParams::new(TR::words__amount.into(), amount, None) + .with_menu_button() + .with_footer(TR::instructions__swipe_up.into(), None) + .with_text_mono(true) + .with_swipe_up() + .with_swipe_down(); + + let mut address_params = ShowInfoParams::new(TR::words__address.into()) + .with_cancel_button() + .with_chunkify(chunkify); + address_params = unwrap!(address_params.add(TR::words__address.into(), address)); + + let flow = flow::new_confirm_output_contact( + paragraphs, + title, + address_params, + amount_params, + )?; + Ok(LayoutObj::new_root(flow)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_confirm_summary(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let amount: TString = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?; @@ -1984,6 +2020,17 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm the recipient, (optionally) confirm the amount and (optionally) confirm the summary and present a Hold to Sign page.""" Qstr::MP_QSTR_flow_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(), + /// def flow_confirm_output_contact( + /// *, + /// title: str, + /// contact_label: str, + /// address: str, + /// amount: str, + /// chunkify: bool = True, + /// ) -> LayoutObj[UiResult]: + /// """Confirm the transaction output for labeled contact.""" + Qstr::MP_QSTR_flow_confirm_output_contact => obj_fn_kw!(0, new_confirm_output_contact).as_obj(), + /// def confirm_summary( /// *, /// amount: str, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 7e4d3eabd3..48bdd49acc 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -522,6 +522,18 @@ def flow_confirm_output( """Confirm the recipient, (optionally) confirm the amount and (optionally) confirm the summary and present a Hold to Sign page.""" +# rust/src/ui/model_mercury/layout.rs +def flow_confirm_output_contact( + *, + title: str, + contact_label: str, + address: str, + amount: str, + chunkify: bool = True, +) -> LayoutObj[UiResult]: + """Confirm the transaction output for labeled contact.""" + + # rust/src/ui/model_mercury/layout.rs def confirm_summary( *, diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py index 4f564f8bed..e41400c316 100644 --- a/core/src/apps/bitcoin/sign_tx/layout.py +++ b/core/src/apps/bitcoin/sign_tx/layout.py @@ -96,9 +96,7 @@ async def confirm_output( title = None address_label = None - if output.label: - address_label = output.label - elif output.address_n and not output.multisig: + if output.address_n and not output.multisig: from trezor import utils # Showing the account string only for model_tr layout @@ -129,6 +127,7 @@ async def confirm_output( chunkify=chunkify, source_account=account_label(coin, address_n), source_account_path=address_n_to_str(address_n) if address_n else None, + contact_label=output.label, ) await layout diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index 3dd1181887..b6e6bc1ed0 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -375,6 +375,7 @@ async def confirm_output( source_account: str | None = None, source_account_path: str | None = None, cancel_text: str | None = None, + contact_label: str | None = None, ) -> None: if address_label is not None: title = address_label @@ -385,29 +386,41 @@ async def confirm_output( else: title = TR.send__title_sending_to - await raise_if_not_confirmed( - 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, - ) + if contact_label: + await raise_if_not_confirmed( + trezorui2.flow_confirm_output_contact( + title = "Send", + contact_label=contact_label, + address=address, + amount=amount, + chunkify=chunkify, + ), + br_name=None, + ) + else: + await raise_if_not_confirmed( + 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, + ) async def should_show_payment_request_details(