refactor: replace calls to `flow_confirm_output`
What changed, and why it matters
This commit refactors how Trezor hardware wallets display transaction confirmation screens for Ethereum and Solana operations. It replaces an older Rust-based confirmation flow with a newer MicroPython-based linear flow. The change is described by the developer as a cleanup step to remove an old UI component. There is no direct evidence in the commit that this fixes a security vulnerability, but any change to transaction confirmation logic carries a risk of accidentally altering what users see or approve.
Treat as a routine refactor with indirect security relevance. Reviewers should verify that the new `confirm_linear_flow` sequences present the same critical transaction data (recipient, amount, maximum fee, account/path info) to the user in the same order and with equivalent confirmation semantics, and that no button-request or cancellation behavior has changed in a way that could be exploited. No immediate security response is indicated by the diff alone.
Security signals we found
UI flow refactor for transaction confirmation screens
Replacement of Rust-backed `flow_confirm_output` with uPy `confirm_linear_flow`
Changes to Ethereum send, Ethereum staking, and Solana staking confirmation flows
No changelog entry and no security mention in commit message
Potential for behavioral differences in how account info, fee details, and summary items are presented to the user
Evidence from the diff
The commit replaces calls to trezorui_api.flow_confirm_output (which used the Rust ConfirmOutputWithSummary flow when summary_items were passed) with confirm_linear_flow composed of confirm_value/confirm_total or confirm_summary calls in the delizia and eckhart UI layout modules. It affects Ethereum send/staking flows and Solana staking flows. The diff shows parameter remapping, new helper _get_account_info_items, and for eckhart the introduction of menu-based interactions. No changelog entry is provided and no security relevance is stated.
Changed components
core/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pyEthereum transaction confirmation UIEthereum staking confirmation UISolana staking confirmation UIInspect captured patch +279 / −173
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 7149dab9..0216234e 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -1038,6 +1038,31 @@ async def confirm_trade(
if not utils.BITCOIN_ONLY:
+ def _get_account_info_items(
+ account: str | None, account_path: str | None, title: str | None = None
+ ) -> list[tuple[str, list[StrPropertyType], str | None]]:
+ account_properties: list[StrPropertyType] = []
+ if account:
+ account_properties.append((TR.words__account, account, None))
+ if account_path:
+ account_properties.append(
+ (
+ TR.address_details__derivation_path,
+ account_path,
+ None,
+ )
+ )
+ if account_properties:
+ return [
+ (
+ TR.address_details__account_info,
+ account_properties,
+ title,
+ )
+ ]
+ else:
+ return []
+
def confirm_ethereum_unknown_contract_warning(
_title: str | None,
) -> Awaitable[None]:
@@ -1059,41 +1084,33 @@ if not utils.BITCOIN_ONLY:
br_code: ButtonRequestType = ButtonRequestType.SignTx,
chunkify: bool = False,
) -> None:
- fee_items: list[PropertyType] | None = (
- list(fee_info_items) if fee_info_items else None
- )
- summary_items: list[PropertyType] | None = [
- (TR.words__amount, total_amount, None),
- (TR.send__maximum_fee, maximum_fee, None),
- ]
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=TR.words__address,
+ await confirm_linear_flow(
+ lambda: confirm_value(
+ TR.words__address,
+ recipient or TR.ethereum__new_contract,
+ description="",
+ br_name="confirm_output",
+ br_code=br_code,
subtitle=(
TR.words__recipient
if is_send
else TR.ethereum__interaction_contract
),
- description=None,
- extra=None,
- message=(recipient or TR.ethereum__new_contract),
- chunkify=(chunkify if recipient else False),
- text_mono=True,
- account_title=TR.send__send_from,
- account=account,
- account_path=account_path,
- address_item=None,
- extra_item=None,
- br_code=ButtonRequestType.SignTx,
- br_name="confirm_output",
- summary_items=summary_items,
- fee_items=fee_items,
- summary_title=TR.words__title_summary,
- summary_br_name="confirm_total",
- summary_br_code=ButtonRequestType.SignTx,
+ chunkify=chunkify if recipient else False,
cancel_text=TR.buttons__cancel,
+ info_items=_get_account_info_items(
+ account, account_path, TR.send__send_from
+ ),
+ ),
+ lambda: confirm_total(
+ total_amount,
+ maximum_fee,
+ title=None,
+ total_label=TR.words__amount,
+ fee_label=TR.send__maximum_fee,
+ fee_items=fee_info_items,
+ back_button=True,
),
- None,
)
def ethereum_address_title() -> str:
@@ -1235,45 +1252,43 @@ if not utils.BITCOIN_ONLY:
maximum_fee: str,
address: str,
address_title: str,
- info_items: Iterable[StrPropertyType],
+ fee_info_items: Iterable[StrPropertyType],
chunkify: bool = False,
br_name: str = "confirm_ethereum_staking_tx",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
- summary_items: list[PropertyType] = []
- if verb == TR.ethereum__staking_claim:
- summary_items.extend([(TR.send__maximum_fee, maximum_fee, None)])
- else:
- summary_items.extend(
- [
- (TR.words__amount, total_amount, None),
- (TR.send__maximum_fee, maximum_fee, None),
- ]
- )
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=verb,
+ assert verb in (
+ TR.ethereum__staking_claim,
+ TR.ethereum__staking_stake,
+ TR.ethereum__staking_unstake,
+ )
+
+ address_info_items: list[tuple[str, list[StrPropertyType], None]] = [
+ (address_title, [(address_title, address, chunkify)], None)
+ ]
+ await confirm_linear_flow(
+ lambda: confirm_value(
+ verb,
+ intro_question,
+ description="",
+ br_name=br_name,
+ br_code=br_code,
subtitle=None,
- description=None,
- extra=None,
- message=intro_question,
+ is_data=False,
chunkify=False,
- text_mono=False,
- account_title=TR.address_details__account_info,
- account=account,
- account_path=account_path,
- br_code=br_code,
- br_name=br_name,
- address_item=(address_title, address, None),
- extra_item=None,
- summary_items=summary_items,
- fee_items=list(info_items) if info_items else None,
- summary_title=verb,
- summary_br_name="confirm_total",
- summary_br_code=ButtonRequestType.SignTx,
- cancel_text=TR.buttons__cancel, # cancel staking
+ cancel_text=TR.buttons__cancel,
+ info_items=address_info_items
+ + _get_account_info_items(account, account_path),
+ ),
+ lambda: confirm_total(
+ total_amount if not verb == TR.ethereum__staking_claim else None,
+ maximum_fee,
+ title=verb,
+ total_label=TR.words__amount,
+ fee_label=TR.send__maximum_fee,
+ fee_items=fee_info_items,
+ back_button=True,
),
- br_name=None,
)
def confirm_solana_unknown_token_warning() -> Awaitable[None]:
@@ -1338,34 +1353,36 @@ if not utils.BITCOIN_ONLY:
br_name: str = "confirm_solana_staking_tx",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
- summary_items: list[StrPropertyType] = []
- if amount_item:
- summary_items.append(amount_item)
- summary_items.append(fee_item)
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=title,
- subtitle=None,
- description=description,
- extra=f"\n{TR.words__provider}:" if vote_account else None,
- message=vote_account,
- chunkify=True,
- text_mono=True,
- account_title=TR.address_details__account_info,
- account=account,
- account_path=account_path,
- br_code=br_code,
+ info_items = []
+ if stake_item:
+ info_items.append((stake_item[0], [stake_item], None))
+ info_items.extend(_get_account_info_items(account, account_path))
+
+ extra = TR.words__provider if vote_account else ""
+
+ await confirm_linear_flow(
+ lambda: confirm_value(
+ title,
+ vote_account,
+ subtitle=description if extra else None,
br_name=br_name,
- address_item=stake_item,
- extra_item=blockhash_item,
- fee_items=list(fee_details) if fee_details else None,
- summary_title=title,
- summary_items=summary_items,
- summary_br_name="confirm_total",
- summary_br_code=ButtonRequestType.SignTx,
+ br_code=br_code,
+ description=extra or description,
+ is_data=False,
+ chunkify=True,
cancel_text=TR.buttons__cancel,
+ info_items=info_items,
+ ),
+ lambda: confirm_total(
+ amount_item[1] if amount_item else None,
+ fee_item[1] or "",
+ title=title,
+ total_label=amount_item[0] if amount_item else None,
+ account_title=blockhash_item[0],
+ account_items=[blockhash_item],
+ fee_label=fee_item[0],
+ fee_items=fee_details,
),
- br_name=None,
)
def confirm_cardano_tx(
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 1f79f122..3240f515 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -1049,6 +1049,22 @@ def confirm_trade(
if not utils.BITCOIN_ONLY:
+ def _get_account_info_items(
+ account: str | None, account_path: str | None
+ ) -> list[StrPropertyType]:
+ account_properties: list[StrPropertyType] = []
+ if account:
+ account_properties.append((TR.words__account, account, None))
+ if account_path:
+ account_properties.append(
+ (
+ TR.address_details__derivation_path,
+ account_path,
+ None,
+ )
+ )
+ return account_properties
+
def confirm_ethereum_unknown_contract_warning(title: str | None) -> Awaitable[None]:
return show_danger(
"unknown_contract_warning",
@@ -1077,42 +1093,60 @@ if not utils.BITCOIN_ONLY:
br_code: ButtonRequestType = ButtonRequestType.SignTx,
chunkify: bool = False,
) -> None:
+ from trezor.ui.layouts.menu import Menu, interact_with_menu
+
subtitle = (
None
if not is_send and recipient is None
else (TR.words__recipient if is_send else TR.ethereum__interaction_contract)
)
title = TR.words__send
- fee_items: list[PropertyType] | None = (
- list(fee_info_items) if fee_info_items else None
- )
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=title,
- subtitle=subtitle,
- description=None,
- extra=None,
- message=(recipient or TR.ethereum__new_contract),
- chunkify=(chunkify if recipient else False),
- text_mono=(True if recipient else False),
- account_title=TR.send__send_from,
- account=account,
- account_path=account_path,
- address_item=None,
- extra_item=None,
- br_code=br_code,
- br_name="confirm_output",
- summary_items=[
- (TR.words__amount, total_amount, True),
- (TR.send__maximum_fee, maximum_fee, True),
- ],
- fee_items=fee_items,
- summary_title=title,
- summary_br_name=br_name,
- summary_br_code=br_code,
- cancel_text=TR.buttons__cancel,
+
+ account_properties = _get_account_info_items(account, account_path)
+ 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 = []
+
+ await confirm_linear_flow(
+ lambda: interact_with_menu(
+ trezorui_api.confirm_value(
+ title=title,
+ value=recipient or TR.ethereum__new_contract,
+ is_data=bool(recipient),
+ description="",
+ subtitle=subtitle,
+ verb=TR.buttons__continue,
+ info=False,
+ hold=False,
+ chunkify=chunkify if recipient else False,
+ external_menu=True,
+ ),
+ Menu.root(menu_items, TR.send__cancel_sign),
+ "confirm_output",
+ br_code,
+ ),
+ lambda: interact(
+ trezorui_api.confirm_summary(
+ amount=total_amount,
+ amount_label=TR.words__amount,
+ fee=maximum_fee,
+ fee_label=TR.send__maximum_fee,
+ extra_title=TR.confirm_total__title_fee,
+ extra_items=list(fee_info_items),
+ title=title,
+ back_button=True,
+ ),
+ br_name,
+ br_code,
),
- None,
)
def ethereum_address_title() -> str:
@@ -1272,41 +1306,59 @@ if not utils.BITCOIN_ONLY:
br_name: str = "confirm_ethereum_staking_tx",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
- summary_items: list[PropertyType] = []
- if verb == TR.ethereum__staking_claim:
- summary_items.extend([(TR.send__maximum_fee, maximum_fee, True)])
- else:
- summary_items.extend(
- [
- (TR.words__amount, total_amount, True),
- (TR.send__maximum_fee, maximum_fee, True),
- ]
+ from trezor.ui.layouts.menu import Menu, interact_with_menu
+
+ assert verb in (
+ TR.ethereum__staking_claim,
+ TR.ethereum__staking_stake,
+ TR.ethereum__staking_unstake,
+ )
+ menu_items = [create_details(address_title, address, None)]
+ account_properties = _get_account_info_items(account, account_path)
+ if account_properties:
+ menu_items.append(
+ create_details(
+ TR.address_details__account_info,
+ account_properties,
+ title=TR.address_details__account_info,
+ subtitle=TR.send__send_from,
+ )
)
- fee_items: list[PropertyType] | None = list(info_items) if info_items else None
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=verb,
- subtitle=None,
- description=None,
- extra=None,
- message=intro_question,
- chunkify=False,
- text_mono=False,
- account_title=TR.address_details__account_info,
- account=account,
- account_path=account_path,
- br_code=br_code,
- br_name=br_name,
- address_item=(address_title, address, True),
- extra_item=None,
- summary_items=summary_items,
- fee_items=fee_items,
- summary_title=verb,
- summary_br_name="confirm_total",
- summary_br_code=ButtonRequestType.SignTx,
- cancel_text=TR.buttons__cancel, # cancel staking
+
+ amount, amount_label = (
+ total_amount,
+ TR.words__amount if not verb == TR.ethereum__staking_claim else None,
+ )
+
+ await confirm_linear_flow(
+ lambda: interact_with_menu(
+ trezorui_api.confirm_value(
+ title=verb,
+ value=intro_question,
+ is_data=False,
+ description="",
+ subtitle=None,
+ chunkify=False,
+ external_menu=True,
+ ),
+ Menu.root(menu_items, TR.send__cancel_sign),
+ br_name,
+ ButtonRequestType.SignTx,
+ ),
+ lambda: interact(
+ trezorui_api.confirm_summary(
+ amount=amount,
+ amount_label=amount_label,
+ fee=maximum_fee,
+ fee_label=TR.send__maximum_fee,
+ extra_title=TR.confirm_total__title_fee,
+ extra_items=list(info_items),
+ title=title,
+ back_button=True,
+ ),
+ "confirm_total",
+ br_code,
),
- br_name=None,
)
def confirm_solana_recipient(
@@ -1364,33 +1416,70 @@ if not utils.BITCOIN_ONLY:
br_name: str = "confirm_solana_staking_tx",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
- summary_items: list[StrPropertyType] = [fee_item]
- if amount_item:
- summary_items.append(amount_item)
- await raise_if_not_confirmed(
- trezorui_api.flow_confirm_output(
- title=title,
- subtitle=None,
- description=description,
- extra=TR.words__provider if vote_account else None,
- message=vote_account,
- chunkify=True,
- text_mono=True,
- account_title=TR.address_details__account_info,
- account=account,
- account_path=account_path,
- br_code=br_code,
- br_name=br_name,
- address_item=stake_item,
- extra_item=blockhash_item,
- fee_items=list(fee_details) if fee_details else None,
- summary_title=title,
- summary_items=summary_items,
- summary_br_name="confirm_total",
- summary_br_code=ButtonRequestType.SignTx,
- cancel_text=TR.buttons__cancel,
+ from trezor.ui.layouts.menu import Menu, interact_with_menu
+
+ menu_items = [
+ create_details(
+ TR.address_details__account_info,
+ _get_account_info_items(account, account_path),
+ title=TR.address_details__account_info,
+ subtitle=TR.send__send_from,
+ )
+ ]
+ if stake_item:
+ menu_items.append(
+ create_details(
+ stake_item[0] or "", [(None, stake_item[1], stake_item[2])], None
+ )
+ )
+
+ summary_menu_items = [
+ create_details(blockhash_item[0] or "", [blockhash_item], None),
+ create_details(TR.confirm_total__title_fee, list(fee_details), None),
+ ]
+
+ extra = TR.words__provider if vote_account else ""
+
+ await confirm_linear_flow(
+ lambda: interact_with_menu(
+ (
+ trezorui_api.confirm_value(
+ title=title,
+ value=vote_account,
+ extra=extra,
+ description=description,
+ is_data=False,
+ chunkify=True,
+ external_menu=True,
+ verb=TR.buttons__continue,
+ )
+ if extra
+ else trezorui_api.confirm_action(
+ title=title,
+ action=vote_account,
+ description=description,
+ verb=TR.buttons__continue,
+ external_menu=True,
+ )
+ ),
+ Menu.root(menu_items, TR.send__cancel_sign),
+ br_name,
+ br_code,
+ ),
+ lambda: interact_with_menu(
+ trezorui_api.confirm_summary(
+ amount=amount_item[1] if amount_item else None,
+ amount_label=amount_item[0] if amount_item else None,
+ fee=fee_item[1] or "",
+ fee_label=fee_item[0] or "",
+ title=title,
+ back_button=True,
+ external_menu=True,
+ ),
+ Menu.root(summary_menu_items, TR.buttons__cancel),
+ br_name,
+ br_code,
),
- br_name=None,
)
def confirm_cardano_tx(
Why this scored 31/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.