refactor: drop `ConfirmOutputWithAmount`
What changed, and why it matters
This commit removes a dedicated two-page 'confirm output with amount' screen and replaces it with two separate confirmation prompts for the address and the amount. It is a user-interface refactor, not a fix for a security vulnerability. The change does not introduce obvious security flaws, but it slightly alters how users review transaction details on newer Trezor layouts.
No immediate security action required. Treat as a normal UI refactor. If reviewing for release, verify that the new two-step confirmation still requires explicit user approval for both address and amount, and that automated device tests pass.
Security signals we found
UI flow refactor with no changelog entry
Removal of dedicated amount-confirmation page in favor of separate confirm_value calls
Test expectations updated to include extra ConfirmOutput button requests
No cryptographic, parsing, or authorization logic changed
Evidence from the diff
The commit refactors the ConfirmOutputWithAmount flow out of the Delizia and Eckhart UI layouts. Instead of a single flow that shows the address and then the amount on consecutive pages, the Python layer now calls confirm_value twice when an amount is present: once for the address and once for the amount. The Rust UI trait flow_confirm_output loses its amount parameter, and the corresponding ConfirmOutputWithAmount state machines are deleted. Tests are updated to expect additional button requests and input-flow yields. There is no evidence of a security bug being fixed or introduced.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/flow/confirm_output.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/embed/rust/src/ui/ui_firmware.rscore/mocks/generated/trezorui_api.pyicore/src/apps/nostr/sign_event.pycore/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pytests/device_tests/bitcoin/test_signtx.pytests/input_flows.pyInspect captured patch +183 / −263
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 207c7f8c..aa697b63 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -528,7 +528,6 @@ extern "C" fn new_flow_confirm_output(n_args: usize, args: *const Obj, kwargs: *
let description: Option<TString> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let message: TString = kwargs.get(Qstr::MP_QSTR_message)?.try_into()?;
- let amount: Option<TString> = kwargs.get(Qstr::MP_QSTR_amount)?.try_into_option()?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let text_mono: bool = kwargs.get_or(Qstr::MP_QSTR_text_mono, true)?;
let account_title: TString = kwargs.get(Qstr::MP_QSTR_account_title)?.try_into()?;
@@ -561,7 +560,6 @@ extern "C" fn new_flow_confirm_output(n_args: usize, args: *const Obj, kwargs: *
description,
extra,
message,
- amount,
chunkify,
text_mono,
account_title,
@@ -1728,7 +1726,6 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// message: str,
/// description: str | None,
/// extra: str | None,
- /// amount: str | None,
/// chunkify: bool,
/// text_mono: bool,
/// account_title: str,
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index b4bca947..3522dc4a 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -583,7 +583,6 @@ impl FirmwareUI for UIBolt {
_description: Option<TString<'static>>,
_extra: Option<TString<'static>>,
_message: TString<'static>,
- _amount: Option<TString<'static>>,
_chunkify: bool,
_text_mono: bool,
_account_title: TString<'static>,
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index 44288a07..e35b7402 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -709,7 +709,6 @@ impl FirmwareUI for UICaesar {
_description: Option<TString<'static>>,
_extra: Option<TString<'static>>,
_message: TString<'static>,
- _amount: Option<TString<'static>>,
_chunkify: bool,
_text_mono: bool,
_account_title: TString<'static>,
diff --git a/core/embed/rust/src/ui/layout_delizia/flow/confirm_output.rs b/core/embed/rust/src/ui/layout_delizia/flow/confirm_output.rs
index a2cb1a37..b3bbb563 100644
--- a/core/embed/rust/src/ui/layout_delizia/flow/confirm_output.rs
+++ b/core/embed/rust/src/ui/layout_delizia/flow/confirm_output.rs
@@ -66,43 +66,6 @@ impl FlowController for ConfirmOutput {
}
}
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum ConfirmOutputWithAmount {
- Address,
- Amount,
- Menu,
- AccountInfo,
- CancelTap,
-}
-
-impl FlowController for ConfirmOutputWithAmount {
- #[inline]
- fn index(&'static self) -> usize {
- *self as usize
- }
-
- fn handle_swipe(&'static self, direction: Direction) -> Decision {
- match (self, direction) {
- (Self::Address, Direction::Up) => Self::Amount.swipe(direction),
- (Self::Amount, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
- (Self::Amount, Direction::Down) => Self::Address.swipe(direction),
- _ => self.do_nothing(),
- }
- }
-
- fn handle_event(&'static self, msg: FlowMsg) -> Decision {
- match (self, msg) {
- (_, FlowMsg::Info) => Self::Menu.goto(),
- (Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => Self::CancelTap.swipe_left(),
- (Self::Menu, FlowMsg::Choice(MENU_ITEM_ACCOUNT_INFO)) => Self::AccountInfo.goto(),
- (Self::Menu, FlowMsg::Cancelled) => Self::Address.swipe_right(),
- (Self::CancelTap, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Cancelled),
- (_, FlowMsg::Cancelled) => Self::Menu.goto(),
- _ => self.do_nothing(),
- }
- }
-}
-
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmOutputWithSummary {
Main,
@@ -202,7 +165,6 @@ pub fn new_confirm_output(
account_path: Option<TString<'static>>,
br_name: TString<'static>,
br_code: u16,
- confirm_amount: Option<ConfirmValue>,
confirm_address: Option<ConfirmValue>,
confirm_extra: Option<ConfirmValue>,
summary_items_params: Option<ShowInfoParams>,
@@ -246,19 +208,7 @@ pub fn new_confirm_output(
let ac = AddressDetails::new(account_title, account, account_path)?;
let account_content = ac.map(|_| Some(FlowMsg::Cancelled));
- let res = if let Some(confirm_amount) = confirm_amount {
- let confirm_amount = confirm_amount
- .into_layout()?
- .one_button_request(ButtonRequest::from_num(br_code, br_name));
-
- let mut flow = SwipeFlow::new(&ConfirmOutputWithAmount::Address)?;
- flow.add_page(&ConfirmOutputWithAmount::Address, main_content)?
- .add_page(&ConfirmOutputWithAmount::Amount, confirm_amount)?
- .add_page(&ConfirmOutputWithAmount::Menu, content_main_menu)?
- .add_page(&ConfirmOutputWithAmount::AccountInfo, account_content)?
- .add_page(&ConfirmOutputWithAmount::CancelTap, get_cancel_page())?;
- flow
- } else if let Some(summary_items_params) = summary_items_params {
+ let res = if let Some(summary_items_params) = summary_items_params {
// Summary
let content_summary = summary_items_params
.into_layout()?
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 11fe9247..fea246d8 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -547,7 +547,6 @@ impl FirmwareUI for UIDelizia {
description: Option<TString<'static>>,
extra: Option<TString<'static>>,
message: TString<'static>,
- amount: Option<TString<'static>>,
chunkify: bool,
text_mono: bool,
account_title: TString<'static>,
@@ -578,15 +577,6 @@ impl FirmwareUI for UIDelizia {
.with_chunkify(chunkify)
.with_text_mono(text_mono);
- let confirm_amount = amount.map(|amount| {
- ConfirmValue::new(TR::words__amount.into(), amount.into(), None)
- .with_subtitle(subtitle)
- .with_menu_button()
- .with_swipeup_footer(None)
- .with_text_mono(text_mono)
- .with_swipe_down()
- });
-
let confirm_address = address_item.map(|address_item| {
let [key, value, _is_data]: [Obj; 3] = unwrap!(util::iter_into_array(address_item));
ConfirmValue::new(
@@ -643,7 +633,6 @@ impl FirmwareUI for UIDelizia {
account_path,
br_name,
br_code,
- confirm_amount,
confirm_address,
confirm_extra,
summary_items_params,
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
index 3d277ca0..2546f1ac 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
@@ -73,58 +73,6 @@ impl FlowController for ConfirmOutput {
}
}
-#[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,
@@ -254,7 +202,6 @@ pub fn new_confirm_output(
title: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
main_paragraphs: ParagraphVecShort<'static>,
- amount: Option<TString<'static>>,
br_name: TString<'static>,
br_code: u16,
account_title: TString<'static>,
@@ -310,77 +257,7 @@ pub fn new_confirm_output(
))
.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),
- ]);
-
- let content_amount = TextScreen::new(
- amount_paragraphs
- .into_paragraphs()
- .with_placement(LinearPlacement::vertical()),
- )
- .with_flow_menu()
- .with_header(Header::new(TR::words__send.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));
-
- 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()
- .map_or_else(ParagraphVecShort::new, |p| p),
- ),
- )?
- .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()
- .map_or_else(ParagraphVecShort::new, |p| p),
- ),
- )?
- .add_page(&ConfirmOutputWithAmount::AmountCancel, content_cancel())?
- .add_page(&ConfirmOutputWithAmount::Cancelled, content_cancelled)?;
- flow
- } else if let Some(summary_paragraphs) = summary_paragraphs {
+ let res = if let Some(summary_paragraphs) = summary_paragraphs {
// Summary
let content_summary = TextScreen::new(
summary_paragraphs
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index 5ebbada9..03189f8d 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -464,7 +464,7 @@ impl FirmwareUI for UIEckhart {
return Err(Error::NotImplementedError);
}
- let paragraphs = ConfirmValueParams {
+ let mut paragraphs = ConfirmValueParams {
description: description.unwrap_or("".into()),
extra: extra.unwrap_or("".into()),
value: if value != Obj::const_none() {
@@ -479,12 +479,18 @@ impl FirmwareUI for UIEckhart {
} else {
&theme::TEXT_MONO_MEDIUM_LIGHT
},
- description_font: &theme::TEXT_SMALL,
+ description_font: if subtitle.is_some() {
+ &theme::TEXT_SMALL_LIGHT
+ } else {
+ &theme::TEXT_SMALL
+ },
extra_font: &theme::TEXT_SMALL,
}
.into_paragraphs()
- .with_placement(LinearPlacement::vertical())
- .with_spacing(theme::PROP_INNER_SPACING);
+ .with_placement(LinearPlacement::vertical());
+ if subtitle.is_none() {
+ paragraphs = paragraphs.with_spacing(theme::PROP_INNER_SPACING);
+ }
let mut right_button = if hold {
let verb = verb.unwrap_or(TR::buttons__hold_to_confirm.into());
@@ -661,7 +667,6 @@ impl FirmwareUI for UIEckhart {
description: Option<TString<'static>>,
extra: Option<TString<'static>>,
message: TString<'static>,
- amount: Option<TString<'static>>,
chunkify: bool,
text_mono: bool,
account_title: TString<'static>,
@@ -792,7 +797,6 @@ impl FirmwareUI for UIEckhart {
title,
subtitle,
main_paragraphs,
- amount,
br_name,
br_code,
account_title,
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 4cad8bca..471eb116 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -188,7 +188,6 @@ pub trait FirmwareUI {
description: Option<TString<'static>>,
extra: Option<TString<'static>>,
message: TString<'static>,
- amount: Option<TString<'static>>,
chunkify: bool,
text_mono: bool,
account_title: TString<'static>,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index dae78aa6..65f30ccb 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -356,7 +356,6 @@ def flow_confirm_output(
message: str,
description: str | None,
extra: str | None,
- amount: str | None,
chunkify: bool,
text_mono: bool,
account_title: str,
diff --git a/core/src/apps/nostr/sign_event.py b/core/src/apps/nostr/sign_event.py
index edfa8be4..d061a4de 100644
--- a/core/src/apps/nostr/sign_event.py
+++ b/core/src/apps/nostr/sign_event.py
@@ -4,7 +4,6 @@ from apps.common.keychain import auto_keychain
if TYPE_CHECKING:
from trezor.messages import NostrEventSignature, NostrSignEvent
- from trezor.ui.layouts import PropertyType
from apps.common.keychain import Keychain
@@ -41,9 +40,9 @@ async def sign_event(msg: NostrSignEvent, keychain: Keychain) -> NostrEventSigna
["[" + ",".join(f'"{t}"' for t in tag) + "]" for tag in tags]
)
- info_items: list[PropertyType] = [
- ("Created", str(created_at), True),
- ("Tags", serialized_tags, True),
+ info_items = [
+ ("Created", str(created_at), None),
+ ("Tags", serialized_tags, None),
]
await confirm_value(title, content, "", "nostr_sign_event", info_items=info_items)
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 1b6e56f8..214caf2f 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -592,32 +592,76 @@ async def confirm_output(
else:
title = TR.send__title_sending_to
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=TR.words__address,
+ if amount is not None:
+ account_properties: list[PropertyType] = []
+ if source_account:
+ account_properties.append((TR.words__account, source_account, None))
+ if source_account_path:
+ account_properties.append(
+ (
+ TR.address_details__derivation_path,
+ source_account_path,
+ None,
+ )
+ )
+ if account_properties:
+ info_items = [
+ (
+ TR.address_details__account_info,
+ account_properties,
+ TR.send__send_from,
+ )
+ ]
+ else:
+ info_items = []
+
+ await confirm_value(
+ TR.words__address,
+ address,
+ description or "",
+ "confirm_output",
+ br_code,
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,
- )
+ cancel_text=TR.send__cancel_sign,
+ info_items=info_items,
+ )
+ await confirm_value(
+ TR.words__amount,
+ amount,
+ "",
+ "confirm_output",
+ br_code,
+ subtitle=title,
+ cancel_text=TR.send__cancel_sign,
+ info_items=info_items,
+ )
+ else:
+ await raise_if_not_confirmed(
+ trezorui_api.flow_confirm_output(
+ title=TR.words__address,
+ subtitle=title,
+ message=address,
+ extra=None,
+ 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_more(
@@ -792,8 +836,11 @@ def confirm_value(
hold: bool = False,
is_data: bool = True,
chunkify: bool = False,
- info_items: Iterable[PropertyType] | None = None,
+ info_items: (
+ Iterable[tuple[str, StrOrBytes | list[PropertyType], str | None]] | None
+ ) = None,
cancel: bool = False,
+ cancel_text: str | None = None,
) -> Awaitable[ui.UiResult]:
"""General confirmation dialog, used by many other confirm_* functions."""
@@ -813,9 +860,14 @@ def confirm_value(
)
info_items = info_items or []
+ menu_items = []
+ for name, p, page_title in info_items:
+ menu_items.append(
+ create_details(str(name), p if isinstance(p, list) else str(p), page_title)
+ )
menu = Menu.root(
- (create_details(str(name), str(value)) for name, value, _is_data in info_items),
- cancel=TR.buttons__cancel,
+ menu_items,
+ cancel=(cancel_text or TR.buttons__cancel),
)
return interact_with_menu(main, menu, br_name, br_code)
@@ -993,7 +1045,6 @@ if not utils.BITCOIN_ONLY:
description=None,
extra=None,
message=(recipient or TR.ethereum__new_contract),
- amount=None,
chunkify=(chunkify if recipient else False),
text_mono=True,
account_title=TR.send__send_from,
@@ -1175,7 +1226,6 @@ if not utils.BITCOIN_ONLY:
description=None,
extra=None,
message=intro_question,
- amount=None,
chunkify=False,
text_mono=False,
account_title=TR.address_details__account_info,
@@ -1216,7 +1266,7 @@ if not utils.BITCOIN_ONLY:
br_name=br_name,
br_code=br_code,
verb=TR.buttons__continue,
- info_items=items,
+ info_items=[(str(k), str(v), None) for k, v, _ in items],
)
def confirm_solana_tx(
@@ -1268,7 +1318,6 @@ if not utils.BITCOIN_ONLY:
description=description,
extra=f"\n{TR.words__provider}:" if vote_account else None,
message=vote_account,
- amount=None,
chunkify=True,
text_mono=True,
account_title=TR.address_details__account_info,
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index d225d905..1b665d1d 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -576,6 +576,8 @@ async def confirm_output(
cancel_text: str | None = None,
description: str | None = None,
) -> None:
+ from trezor.ui.layouts.menu import Menu, confirm_with_menu, interact_with_menu
+
if address_label is not None:
title = address_label
elif title is not None:
@@ -585,32 +587,88 @@ async def confirm_output(
else:
title = TR.send__title_sending_to
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=TR.words__send,
- 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,
- )
+ if amount is not None:
+ account_properties: list[PropertyType] = []
+ if source_account:
+ account_properties.append((TR.words__wallet, source_account, None))
+ if source_account_path:
+ account_properties.append(
+ (
+ TR.address_details__derivation_path,
+ source_account_path,
+ None,
+ )
+ )
+ if account_properties:
+ menu_items = [
+ create_details(
+ TR.address_details__account_info,
+ account_properties,
+ title=TR.address_details__account_info,
+ subtitle=TR.send__send_from,
+ )
+ ]
+ else:
+ menu_items = []
+ menu = Menu.root(
+ menu_items,
+ cancel=TR.buttons__cancel,
+ )
+ while True:
+ address_layout = trezorui_api.confirm_value(
+ title=TR.words__send,
+ value=address,
+ description=description,
+ subtitle=title,
+ verb=TR.buttons__continue,
+ chunkify=chunkify,
+ page_counter=True, # TODO: this is for test_cardano_sign_tx_show_details - maybe we can do without?
+ external_menu=True,
+ )
+ await confirm_with_menu(address_layout, menu, "confirm_output", br_code)
+
+ amount_layout = trezorui_api.confirm_value(
+ title=TR.words__send,
+ value=amount,
+ description=TR.words__amount,
+ is_data=False,
+ subtitle=title,
+ external_menu=True,
+ back_button=True,
+ )
+ if (
+ await interact_with_menu(amount_layout, menu, "confirm_output", br_code)
+ is BACK
+ ):
+ continue
+ else:
+ break
+ else:
+ await raise_if_not_confirmed(
+ trezorui_api.flow_confirm_output(
+ title=TR.words__send,
+ subtitle=title,
+ message=address,
+ extra=None,
+ 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_more(
@@ -1005,7 +1063,6 @@ if not utils.BITCOIN_ONLY:
description=None,
extra=None,
message=(recipient or TR.ethereum__new_contract),
- amount=None,
chunkify=(chunkify if recipient else False),
text_mono=(True if recipient else False),
account_title=TR.send__send_from,
@@ -1204,7 +1261,6 @@ if not utils.BITCOIN_ONLY:
description=None,
extra=None,
message=intro_question,
- amount=None,
chunkify=False,
text_mono=False,
account_title=TR.address_details__account_info,
@@ -1289,7 +1345,6 @@ if not utils.BITCOIN_ONLY:
description=description,
extra=TR.words__provider if vote_account else None,
message=vote_account,
- amount=None,
chunkify=True,
text_mono=True,
account_title=TR.address_details__account_info,
diff --git a/tests/device_tests/bitcoin/test_signtx.py b/tests/device_tests/bitcoin/test_signtx.py
index 1350c3fd..0d7a5e97 100644
--- a/tests/device_tests/bitcoin/test_signtx.py
+++ b/tests/device_tests/bitcoin/test_signtx.py
@@ -184,6 +184,8 @@ def test_one_one_fee_back_from_amount(session: Session):
request_input(0),
request_output(0),
messages.ButtonRequest(code=B.ConfirmOutput),
+ messages.ButtonRequest(code=B.ConfirmOutput),
+ messages.ButtonRequest(code=B.ConfirmOutput),
(is_core(session), messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
request_input(0),
diff --git a/tests/input_flows.py b/tests/input_flows.py
index 4050b15e..43fc94d0 100644
--- a/tests/input_flows.py
+++ b/tests/input_flows.py
@@ -1217,9 +1217,11 @@ class InputFlowSignTxBackFromAmount(InputFlowBase):
self.debug.read_layout()
self.debug.click(self.debug.screen_buttons.cancel())
+ yield
self.debug.read_layout()
self.debug.click(self.debug.screen_buttons.ok())
+ yield
self.debug.read_layout()
self.debug.click(self.debug.screen_buttons.ok())
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.