1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-24 19:19:03 +00:00

feat(eckhart): confirm output flow

This commit is contained in:
Lukas Bielesch 2025-03-30 15:38:06 +02:00 committed by Lukáš Bielesch
parent 06437cea27
commit 00aa5774cd
10 changed files with 698 additions and 28 deletions
core
embed/rust
mocks
src/trezor/ui/layouts/eckhart
translations

View File

@ -622,6 +622,8 @@ static void _librust_qstrs(void) {
MP_QSTR_send__maximum_fee;
MP_QSTR_send__receiving_to_multisig;
MP_QSTR_send__send_from;
MP_QSTR_send__send_in_the_app;
MP_QSTR_send__sign_cancelled;
MP_QSTR_send__sign_transaction;
MP_QSTR_send__title_confirm_sending;
MP_QSTR_send__title_joint_transaction;
@ -787,6 +789,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__receive;
MP_QSTR_words__recipient;
MP_QSTR_words__recovery_share;
MP_QSTR_words__send;
MP_QSTR_words__settings;
MP_QSTR_words__sign;
MP_QSTR_words__signer;
@ -802,6 +805,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__title_threshold;
MP_QSTR_words__try_again;
MP_QSTR_words__unknown;
MP_QSTR_words__wallet;
MP_QSTR_words__warning;
MP_QSTR_words__writable;
MP_QSTR_words__yes;

View File

@ -1264,7 +1264,7 @@ pub enum TranslatedString {
pin__cancel_description = 870, // "Continue without PIN"
pin__cancel_info = 871, // "Without a PIN, anyone can access this device."
pin__cancel_setup = 872, // "Cancel PIN setup"
send__cancel_sign = 873, // "Cancel sign"
send__cancel_sign = 873, // {"Bolt": "Cancel sign", "Caesar": "Cancel sign", "Delizia": "Cancel sign", "Eckhart": "Cancel sign?"}
send__send_from = 874, // "Send from"
instructions__hold_to_sign = 875, // "Hold to sign"
confirm_total__fee_rate = 876, // "Fee rate"
@ -1412,6 +1412,10 @@ pub enum TranslatedString {
reset__recovery_share_description = 998, // "A recovery share is a list of words you wrote down when setting up your Trezor."
reset__recovery_share_number = 999, // "Your wallet backup consists of 1 to 16 shares."
words__recovery_share = 1000, // "Recovery share"
send__send_in_the_app = 1001, // "After signing, send the transaction in the app."
send__sign_cancelled = 1002, // "Sign cancelled."
words__send = 1003, // "Send"
words__wallet = 1004, // "Wallet"
}
impl TranslatedString {
@ -2735,7 +2739,14 @@ impl TranslatedString {
Self::pin__cancel_description => "Continue without PIN",
Self::pin__cancel_info => "Without a PIN, anyone can access this device.",
Self::pin__cancel_setup => "Cancel PIN setup",
#[cfg(feature = "layout_bolt")]
Self::send__cancel_sign => "Cancel sign",
#[cfg(feature = "layout_caesar")]
Self::send__cancel_sign => "Cancel sign",
#[cfg(feature = "layout_delizia")]
Self::send__cancel_sign => "Cancel sign",
#[cfg(feature = "layout_eckhart")]
Self::send__cancel_sign => "Cancel sign?",
Self::send__send_from => "Send from",
Self::instructions__hold_to_sign => "Hold to sign",
Self::confirm_total__fee_rate => "Fee rate",
@ -2918,6 +2929,10 @@ impl TranslatedString {
Self::reset__recovery_share_description => "A recovery share is a list of words you wrote down when setting up your Trezor.",
Self::reset__recovery_share_number => "Your wallet backup consists of 1 to 16 shares.",
Self::words__recovery_share => "Recovery share",
Self::send__send_in_the_app => "After signing, send the transaction in the app.",
Self::send__sign_cancelled => "Sign cancelled.",
Self::words__send => "Send",
Self::words__wallet => "Wallet",
}
}
@ -4325,6 +4340,10 @@ impl TranslatedString {
Qstr::MP_QSTR_reset__recovery_share_description => Some(Self::reset__recovery_share_description),
Qstr::MP_QSTR_reset__recovery_share_number => Some(Self::reset__recovery_share_number),
Qstr::MP_QSTR_words__recovery_share => Some(Self::words__recovery_share),
Qstr::MP_QSTR_send__send_in_the_app => Some(Self::send__send_in_the_app),
Qstr::MP_QSTR_send__sign_cancelled => Some(Self::send__sign_cancelled),
Qstr::MP_QSTR_words__send => Some(Self::words__send),
Qstr::MP_QSTR_words__wallet => Some(Self::words__wallet),
_ => None,
}
}

View File

@ -0,0 +1,520 @@
use heapless::Vec;
use crate::{
error,
micropython::obj::Obj,
strutil::TString,
translations::TR,
ui::{
button_request::ButtonRequest,
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs},
ButtonRequestExt, ComponentExt, MsgMap,
},
flow::{
base::{Decision, DecisionBuilder as _},
FlowController, FlowMsg, SwipeFlow,
},
geometry::{Alignment, Direction, LinearPlacement, Offset},
},
};
use super::super::{
component::Button,
firmware::{
ActionBar, Header, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
VerticalMenuScreenMsg,
},
theme,
};
const MENU_ITEM_CANCEL: usize = 0;
const MENU_ITEM_FEE_INFO: usize = 1;
const MENU_ITEM_ADDRESS_INFO: usize = 2;
const MENU_ITEM_ACCOUNT_INFO: usize = 3;
const MENU_ITEM_EXTRA_INFO: usize = 4;
const TIMEOUT_MS: u32 = 2000;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmOutput {
Address,
Menu,
AccountInfo,
Cancel,
Cancelled,
}
impl FlowController for ConfirmOutput {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Address, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Address, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::Cancel.goto(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => Self::AccountInfo.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Address.goto(),
(Self::AccountInfo, FlowMsg::Cancelled) => Self::Menu.goto(),
(Self::Cancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::Cancel, FlowMsg::Cancelled) => Self::Menu.goto(),
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmOutputWithAmount {
Address,
AddressMenu,
AddressAccountInfo,
AddressCancel,
Amount,
AmountMenu,
AmountAccountInfo,
AmountCancel,
Cancelled,
}
impl FlowController for ConfirmOutputWithAmount {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Address, FlowMsg::Confirmed) => Self::Amount.goto(),
(Self::Address, FlowMsg::Info) => Self::AddressMenu.goto(),
(Self::AddressMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::AddressCancel.goto(),
(Self::AddressMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
Self::AddressAccountInfo.goto()
}
(Self::AddressAccountInfo, FlowMsg::Cancelled) => Self::AddressMenu.goto(),
(Self::AddressMenu, FlowMsg::Cancelled) => Self::Address.goto(),
(Self::AddressCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::AddressCancel, FlowMsg::Cancelled) => Self::AddressMenu.goto(),
(Self::Amount, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Amount, FlowMsg::Cancelled) => Self::Address.goto(),
(Self::Amount, FlowMsg::Info) => Self::AmountMenu.goto(),
(Self::AmountMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::AmountCancel.goto(),
(Self::AmountMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
Self::AmountAccountInfo.goto()
}
(Self::AmountAccountInfo, FlowMsg::Cancelled) => Self::AmountMenu.goto(),
(Self::AmountMenu, FlowMsg::Cancelled) => Self::Amount.goto(),
(Self::AmountCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::AmountCancel, FlowMsg::Cancelled) => Self::AmountMenu.goto(),
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmOutputWithSummary {
Main,
MainMenu,
MainMenuCancel,
MainMenuAddresInfo,
MainMenuAccountInfo,
Summary,
SummaryMenu,
SummaryMenuCancel,
SummaryMenuFeeInfo,
SummaryMenuAccountInfo,
Cancelled,
}
impl FlowController for ConfirmOutputWithSummary {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Main, FlowMsg::Confirmed) => Self::Summary.goto(),
(Self::Main, FlowMsg::Info) => Self::MainMenu.goto(),
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::MainMenuCancel.goto(),
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_ADDRESS_INFO)) => {
Self::MainMenuAddresInfo.goto()
}
(Self::MainMenu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => {
Self::MainMenuAccountInfo.goto()
}
(Self::MainMenu, FlowMsg::Cancelled) => Self::Main.goto(),
(Self::MainMenuAccountInfo | Self::MainMenuAddresInfo, FlowMsg::Cancelled) => {
Self::MainMenu.goto()
}
(Self::MainMenuCancel, FlowMsg::Cancelled) => Self::MainMenu.goto(),
(Self::MainMenuCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::Summary, FlowMsg::Info) => Self::SummaryMenu.goto(),
(Self::Summary, FlowMsg::Cancelled) => Self::Main.goto(),
(Self::Summary, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => {
Self::SummaryMenuCancel.goto()
}
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_FEE_INFO)) => {
Self::SummaryMenuFeeInfo.goto()
}
(Self::SummaryMenu, FlowMsg::Choice(MENU_ITEM_EXTRA_INFO)) => {
Self::SummaryMenuAccountInfo.goto()
}
(Self::SummaryMenu, FlowMsg::Cancelled) => Self::Summary.goto(),
(Self::SummaryMenuCancel, FlowMsg::Cancelled) => Self::SummaryMenu.goto(),
(Self::SummaryMenuCancel, FlowMsg::Confirmed) => Self::Cancelled.goto(),
(Self::SummaryMenuAccountInfo | Self::SummaryMenuFeeInfo, FlowMsg::Cancelled) => {
Self::SummaryMenu.goto()
}
(Self::Cancelled, _) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}
fn content_cancel(
) -> MsgMap<TextScreen<Paragraphs<Paragraph<'static>>>, impl Fn(TextScreenMsg) -> Option<FlowMsg>> {
TextScreen::new(
Paragraph::new(&theme::TEXT_REGULAR, TR::send__cancel_sign)
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(TR::words__send.into()))
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CHEVRON_LEFT),
Button::with_text(TR::buttons__cancel.into()).styled(theme::button_cancel()),
))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
_ => None,
})
}
fn content_main_menu(
address_title: TString<'static>,
address_params: bool,
account_params: bool,
cancel_menu_label: TString<'static>,
) -> MsgMap<VerticalMenuScreen, impl Fn(VerticalMenuScreenMsg) -> Option<FlowMsg>> {
let mut main_menu = VerticalMenu::empty();
let mut main_menu_items = Vec::<usize, 3>::new();
if address_params {
main_menu = main_menu.item(
Button::with_text(address_title)
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start),
);
unwrap!(main_menu_items.push(MENU_ITEM_ADDRESS_INFO));
}
if account_params {
main_menu = main_menu.item(
Button::with_text(TR::address_details__account_info.into())
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
unwrap!(main_menu_items.push(MENU_ITEM_ACCOUNT_INFO));
}
main_menu = main_menu.item(
Button::with_text(cancel_menu_label)
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
unwrap!(main_menu_items.push(MENU_ITEM_CANCEL));
VerticalMenuScreen::new(main_menu)
.with_header(Header::new(TString::empty()).with_close_button())
.map(move |msg| match msg {
VerticalMenuScreenMsg::Selected(i) => {
let selected_item = main_menu_items[i];
Some(FlowMsg::Choice(selected_item))
}
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
})
}
fn content_menu_info(
title: TString<'static>,
subtitle: Option<TString<'static>>,
paragraphs: Option<ParagraphVecShort<'static>>,
) -> MsgMap<
TextScreen<Paragraphs<ParagraphVecShort<'static>>>,
impl Fn(TextScreenMsg) -> Option<FlowMsg>,
> {
TextScreen::new(
paragraphs
.map_or_else(ParagraphVecShort::new, |p| p)
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
)
.with_header(Header::new(title).with_close_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.map(|_| Some(FlowMsg::Cancelled))
}
#[allow(clippy::too_many_arguments)]
pub fn new_confirm_output(
title: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
chunkify: bool,
message: Obj,
amount: Option<Obj>,
br_name: TString<'static>,
br_code: u16,
account_title: TString<'static>,
account_paragraphs: Option<ParagraphVecShort<'static>>,
address_title: Option<TString<'static>>,
address_paragraphs: Option<ParagraphVecShort<'static>>,
summary_title: Option<TString<'static>>,
summary_paragraphs: Option<ParagraphVecShort<'static>>,
summary_br_code: Option<u16>,
summary_br_name: Option<TString<'static>>,
fee_params: Option<ParagraphVecShort<'static>>,
cancel_menu_label: Option<TString<'static>>,
) -> Result<SwipeFlow, error::Error> {
let cancel_menu_label = cancel_menu_label.unwrap_or(TR::buttons__cancel.into());
let address_menu_item = address_paragraphs.is_some();
let account_menu_item = account_paragraphs.is_some();
let fee_menu_item = fee_params.is_some();
let address_title = address_title.unwrap_or(TR::words__address.into());
let account_subtitle = Some(TR::send__send_from.into());
// Main
let main_paragraphs = Paragraph::new(
if chunkify {
&theme::TEXT_MONO_ADDRESS_CHUNKS
} else {
&theme::TEXT_MONO_LIGHT
},
message.try_into().unwrap_or(TString::empty()),
);
let content_main = TextScreen::new(
main_paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(title.unwrap_or(TString::empty())).with_menu_button())
.with_action_bar(ActionBar::new_single(Button::with_text(
TR::buttons__continue.into(),
)))
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
})
.one_button_request(ButtonRequest::from_num(br_code, br_name))
.with_pages(|_| 1);
// Cancelled
let content_cancelled = TextScreen::new(
Paragraph::new(&theme::TEXT_REGULAR, TR::send__sign_cancelled)
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(TR::words__title_done.into()).with_icon(theme::ICON_DONE, theme::GREY))
.with_action_bar(ActionBar::new_timeout(
Button::with_text(TR::instructions__continue_in_app.into()),
TIMEOUT_MS,
))
.map(|_| Some(FlowMsg::Confirmed));
let res = if let Some(amount) = amount {
let amount_paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_SMALL_LIGHT, TR::words__amount).no_break(),
Paragraph::new(
&theme::TEXT_MONO_MEDIUM_LIGHT,
amount.try_into().unwrap_or(TString::empty()),
),
]);
let content_amount = TextScreen::new(
amount_paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(TR::words__amount.into()).with_menu_button())
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CHEVRON_UP),
Button::with_text(TR::buttons__confirm.into()).styled(theme::button_confirm()),
))
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
})
.one_button_request(ButtonRequest::from_num(br_code, br_name))
.with_pages(|_| 1);
let mut flow = SwipeFlow::new(&ConfirmOutputWithAmount::Address)?;
flow.add_page(&ConfirmOutputWithAmount::Address, content_main)?
.add_page(
&ConfirmOutputWithAmount::AddressMenu,
content_main_menu(
address_title,
address_menu_item,
account_menu_item,
cancel_menu_label,
),
)?
.add_page(
&ConfirmOutputWithAmount::AddressAccountInfo,
content_menu_info(
TR::address_details__account_info.into(),
account_subtitle,
account_paragraphs.clone(),
),
)?
.add_page(&ConfirmOutputWithAmount::AddressCancel, content_cancel())?
.add_page(&ConfirmOutputWithAmount::Amount, content_amount)?
.add_page(
&ConfirmOutputWithAmount::AmountMenu,
content_main_menu(
address_title,
address_menu_item,
account_menu_item,
cancel_menu_label,
),
)?
.add_page(
&ConfirmOutputWithAmount::AmountAccountInfo,
content_menu_info(account_title, account_subtitle, account_paragraphs.clone()),
)?
.add_page(&ConfirmOutputWithAmount::AmountCancel, content_cancel())?
.add_page(&ConfirmOutputWithAmount::Cancelled, content_cancelled)?;
flow
} else if let Some(summary_paragraphs) = summary_paragraphs {
// Summary
let content_summary = TextScreen::new(
summary_paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
)
.with_header(
Header::new(summary_title.unwrap_or(TR::words__title_summary.into()))
.with_menu_button(),
)
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CHEVRON_UP),
Button::with_text(TR::instructions__hold_to_sign.into())
.styled(theme::button_confirm()),
))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
})
.one_button_request(ButtonRequest::from_num(
summary_br_code.unwrap(),
summary_br_name.unwrap(),
))
.with_pages(|_| 1);
// SummaryMenu
let mut summary_menu = VerticalMenu::empty();
let mut summary_menu_items = Vec::<usize, 3>::new();
if account_menu_item {
summary_menu = summary_menu.item(Button::with_text(account_title));
unwrap!(summary_menu_items.push(MENU_ITEM_EXTRA_INFO));
}
if fee_menu_item {
summary_menu =
summary_menu.item(Button::with_text(TR::confirm_total__title_fee.into()));
unwrap!(summary_menu_items.push(MENU_ITEM_FEE_INFO));
}
summary_menu = summary_menu
.item(Button::with_text(cancel_menu_label).styled(theme::menu_item_title_orange()));
unwrap!(summary_menu_items.push(MENU_ITEM_CANCEL));
let content_summary_menu = VerticalMenuScreen::new(summary_menu)
.with_header(Header::new(TString::empty()).with_close_button())
.map(move |msg| match msg {
VerticalMenuScreenMsg::Selected(i) => {
let selected_item = summary_menu_items[i];
Some(FlowMsg::Choice(selected_item))
}
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
});
let mut flow = SwipeFlow::new(&ConfirmOutputWithSummary::Main)?;
flow.add_page(&ConfirmOutputWithSummary::Main, content_main)?
.add_page(
&ConfirmOutputWithSummary::MainMenu,
content_main_menu(
address_title,
address_menu_item,
account_menu_item,
cancel_menu_label,
),
)?
.add_page(&ConfirmOutputWithSummary::MainMenuCancel, content_cancel())?
.add_page(
&ConfirmOutputWithSummary::MainMenuAddresInfo,
content_menu_info(address_title, None, address_paragraphs),
)?
.add_page(
&ConfirmOutputWithSummary::MainMenuAccountInfo,
content_menu_info(account_title, account_subtitle, account_paragraphs.clone()),
)?
.add_page(&ConfirmOutputWithSummary::Summary, content_summary)?
.add_page(&ConfirmOutputWithSummary::SummaryMenu, content_summary_menu)?
.add_page(
&ConfirmOutputWithSummary::SummaryMenuCancel,
content_cancel(),
)?
.add_page(
&ConfirmOutputWithSummary::SummaryMenuFeeInfo,
content_menu_info(TR::confirm_total__title_fee.into(), None, fee_params),
)?
.add_page(
&ConfirmOutputWithSummary::SummaryMenuAccountInfo,
content_menu_info(account_title, account_subtitle, account_paragraphs),
)?
.add_page(&ConfirmOutputWithSummary::Cancelled, content_cancelled)?;
flow
} else {
let mut flow = SwipeFlow::new(&ConfirmOutput::Address)?;
flow.add_page(&ConfirmOutput::Address, content_main)?
.add_page(
&ConfirmOutput::Menu,
content_main_menu(
address_title,
address_menu_item,
account_menu_item,
cancel_menu_label,
),
)?
.add_page(
&ConfirmOutput::AccountInfo,
content_menu_info(account_title, account_subtitle, account_paragraphs),
)?
.add_page(&ConfirmOutput::Cancel, content_cancel())?
.add_page(&ConfirmOutput::Cancelled, content_cancelled)?;
flow
};
Ok(res)
}

View File

@ -1,3 +1,4 @@
pub mod confirm_output;
pub mod confirm_reset;
pub mod confirm_set_new_pin;
pub mod confirm_summary;
@ -8,6 +9,7 @@ pub mod request_passphrase;
pub mod show_danger;
pub mod show_share_words;
pub use confirm_output::new_confirm_output;
pub use confirm_reset::new_confirm_reset;
pub use confirm_set_new_pin::new_set_new_pin;
pub use confirm_summary::new_confirm_summary;

View File

@ -1,5 +1,4 @@
use core::cmp::Ordering;
use heapless::Vec;
use crate::{
error::Error,
@ -393,29 +392,114 @@ impl FirmwareUI for UIEckhart {
}
fn flow_confirm_output(
_title: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
title: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
_description: Option<TString<'static>>,
_extra: Option<TString<'static>>,
_message: Obj,
_amount: Option<Obj>,
_chunkify: bool,
message: Obj,
amount: Option<Obj>,
chunkify: bool,
_text_mono: bool,
_account_title: TString<'static>,
_account: Option<TString<'static>>,
_account_path: Option<TString<'static>>,
_br_code: u16,
_br_name: TString<'static>,
_address_item: Option<(TString<'static>, Obj)>,
account_title: TString<'static>,
account: Option<TString<'static>>,
account_path: Option<TString<'static>>,
br_code: u16,
br_name: TString<'static>,
address_item: Option<(TString<'static>, Obj)>,
_extra_item: Option<(TString<'static>, Obj)>,
_summary_items: Option<Obj>,
_fee_items: Option<Obj>,
_summary_title: Option<TString<'static>>,
_summary_br_code: Option<u16>,
_summary_br_name: Option<TString<'static>>,
_cancel_text: Option<TString<'static>>,
summary_items: Option<Obj>,
fee_items: Option<Obj>,
summary_title: Option<TString<'static>>,
summary_br_code: Option<u16>,
summary_br_name: Option<TString<'static>>,
cancel_text: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let (address_title, address_paragraphs) = if let Some(address_item) = address_item {
let mut paragraphs = ParagraphVecShort::new();
for pair in IterBuf::new().try_iterate(address_item.1)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_SMALL_LIGHT, label).no_break()));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, value)));
}
(Some(address_item.0), Some(paragraphs))
} else {
(None, None)
};
// collect available info
let account_paragraphs = {
let mut paragraphs = ParagraphVecShort::new();
if let Some(account) = account {
unwrap!(paragraphs.push(
Paragraph::new(
&theme::TEXT_SMALL_LIGHT,
TString::from_translation(TR::words__wallet)
)
.no_break()
));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, account)));
}
if let Some(path) = account_path {
unwrap!(paragraphs.push(
Paragraph::new(
&theme::TEXT_SMALL_LIGHT,
TString::from_translation(TR::address_details__derivation_path)
)
.no_break()
));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, path)));
}
if paragraphs.is_empty() {
None
} else {
Some(paragraphs)
}
};
let summary_paragraphs = if let Some(items) = summary_items {
let mut paragraphs = ParagraphVecShort::new();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_SMALL_LIGHT, label).no_break()));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, value)));
}
Some(paragraphs)
} else {
None
};
let fee_paragraphs = if let Some(items) = fee_items {
let mut paragraphs = ParagraphVecShort::new();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_SMALL_LIGHT, label).no_break()));
unwrap!(paragraphs.push(Paragraph::new(&theme::TEXT_MONO_LIGHT, value)));
}
Some(paragraphs)
} else {
None
};
let flow = flow::confirm_output::new_confirm_output(
title,
subtitle,
chunkify,
message,
amount,
br_name,
br_code,
account_title,
account_paragraphs,
address_title,
address_paragraphs,
summary_title,
summary_paragraphs,
summary_br_code,
summary_br_name,
fee_paragraphs,
cancel_text,
)?;
Ok(flow)
}
fn flow_confirm_set_new_pin(

View File

@ -753,6 +753,8 @@ class TR:
send__maximum_fee: str = "Maximum fee"
send__receiving_to_multisig: str = "Receiving to a multisig address."
send__send_from: str = "Send from"
send__send_in_the_app: str = "After signing, send the transaction in the app."
send__sign_cancelled: str = "Sign cancelled."
send__sign_transaction: str = "Sign transaction"
send__title_confirm_sending: str = "Confirm sending"
send__title_joint_transaction: str = "Joint transaction"
@ -974,6 +976,7 @@ class TR:
words__receive: str = "Receive"
words__recipient: str = "Recipient"
words__recovery_share: str = "Recovery share"
words__send: str = "Send"
words__settings: str = "Settings"
words__sign: str = "Sign"
words__signer: str = "Signer"
@ -989,6 +992,7 @@ class TR:
words__title_threshold: str = "Threshold"
words__try_again: str = "Try again."
words__unknown: str = "Unknown"
words__wallet: str = "Wallet"
words__warning: str = "Warning"
words__writable: str = "Writable"
words__yes: str = "Yes"

View File

@ -392,6 +392,7 @@ async def confirm_output(
source_account: str | None = None,
source_account_path: str | None = None,
cancel_text: str | None = None,
description: str | None = None,
) -> None:
if address_label is not None:
title = address_label
@ -402,9 +403,32 @@ async def confirm_output(
else:
title = TR.send__title_sending_to
# FIXME: not implemented
# use `flow_confirm_output` or `TT/TR` style?
raise NotImplementedError
await raise_if_not_confirmed(
trezorui_api.flow_confirm_output(
title=TR.words__address,
subtitle=title,
message=address,
extra=None,
amount=amount,
chunkify=chunkify,
text_mono=True,
account_title=TR.send__send_from,
account=source_account,
account_path=source_account_path,
address_item=None,
extra_item=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,
description=description,
),
br_name=None,
)
async def should_show_payment_request_details(

View File

@ -812,7 +812,13 @@
"sd_card__wanna_format": "Do you really want to format the SD card?",
"sd_card__wrong_sd_card": "Wrong SD card.",
"send__address_path": "address path",
"send__cancel_sign": "Cancel sign",
"send__cancel_sign": {
"Bolt": "Cancel sign",
"Caesar": "Cancel sign",
"Delizia": "Cancel sign",
"Eckhart": "Cancel sign?"
},
"send__sign_cancelled": "Sign cancelled.",
"send__confirm_sending": "Sending amount",
"send__from_multiple_accounts": "Sending from multiple accounts.",
"send__incl_transaction_fee": "incl. Transaction fee",
@ -821,6 +827,7 @@
"send__receiving_to_multisig": "Receiving to a multisig address.",
"send__send_from": "Send from",
"send__sign_transaction": "Sign transaction",
"send__send_in_the_app": "After signing, send the transaction in the app.",
"send__title_confirm_sending": "Confirm sending",
"send__title_joint_transaction": "Joint transaction",
"send__title_receiving_to": "Receiving to",
@ -1041,6 +1048,7 @@
"words__receive": "Receive",
"words__recipient": "Recipient",
"words__recovery_share": "Recovery share",
"words__send": "Send",
"words__settings": "Settings",
"words__sign": "Sign",
"words__signer": "Signer",
@ -1061,6 +1069,7 @@
"Eckhart": "Try again"
},
"words__unknown": "Unknown",
"words__wallet": "Wallet",
"words__warning": "Warning",
"words__writable": "Writable",
"words__yes": "Yes"

View File

@ -999,5 +999,9 @@
"997": "words__receive",
"998": "reset__recovery_share_description",
"999": "reset__recovery_share_number",
"1000": "words__recovery_share"
"1000": "words__recovery_share",
"1001": "send__send_in_the_app",
"1002": "send__sign_cancelled",
"1003": "words__send",
"1004": "words__wallet"
}

View File

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "ff202420b22b94cfa9975b944d0c852f812142957abff7444d85979d039547fb",
"datetime": "2025-03-26T11:49:54.296786",
"commit": "0d126a51dd42476e785a551bfde802f46a5809df"
"merkle_root": "509a95ff1557b28e4b5d3e884edcaecc80f71cf980c636c42458a5dbf3b4fca5",
"datetime": "2025-03-30T13:35:55.278368",
"commit": "191a81ae4072b1298839120ee2301e120d1cd660"
},
"history": [
{