From bcb353a4a117a0f5a3cb61f506d8dcdc3740230a Mon Sep 17 00:00:00 2001 From: grdddj Date: Thu, 14 Sep 2023 12:25:13 +0200 Subject: [PATCH] feat(core): support optional chunkification of addresses in receive and send flows --- core/embed/rust/librust_qstr.h | 1 + .../rust/src/ui/component/text/layout.rs | 81 ++++++++++++++++++- core/embed/rust/src/ui/component/text/op.rs | 28 ++++++- core/embed/rust/src/ui/model_tr/layout.rs | 29 +++++-- core/embed/rust/src/ui/model_tr/theme.rs | 12 ++- core/embed/rust/src/ui/model_tt/layout.rs | 34 +++++++- core/embed/rust/src/ui/model_tt/theme.rs | 14 +++- core/mocks/generated/trezorui2.pyi | 4 + core/src/apps/binance/get_address.py | 4 +- core/src/apps/binance/layout.py | 13 ++- core/src/apps/bitcoin/get_address.py | 2 + core/src/apps/bitcoin/keychain.py | 6 +- core/src/apps/bitcoin/sign_tx/approvers.py | 7 +- core/src/apps/bitcoin/sign_tx/helpers.py | 7 +- core/src/apps/bitcoin/sign_tx/layout.py | 15 +++- core/src/apps/cardano/get_address.py | 4 +- core/src/apps/cardano/layout.py | 4 + core/src/apps/cardano/sign_tx/signer.py | 2 + core/src/apps/ethereum/get_address.py | 4 +- core/src/apps/monero/get_address.py | 1 + core/src/apps/monero/layout.py | 6 +- core/src/apps/nem/get_address.py | 1 + core/src/apps/nem/sign_tx.py | 4 +- core/src/apps/nem/transfer/__init__.py | 3 +- core/src/apps/nem/transfer/layout.py | 2 + core/src/apps/ripple/get_address.py | 6 +- core/src/apps/ripple/layout.py | 4 +- core/src/apps/ripple/sign_tx.py | 4 +- core/src/apps/stellar/get_address.py | 4 +- core/src/apps/tezos/get_address.py | 6 +- core/src/apps/tezos/layout.py | 3 +- core/src/apps/tezos/sign_tx.py | 8 +- core/src/trezor/ui/layouts/tr/__init__.py | 5 ++ core/src/trezor/ui/layouts/tt_v2/__init__.py | 5 ++ 34 files changed, 292 insertions(+), 41 deletions(-) diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 81ad21bf34..a26dc21659 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -36,6 +36,7 @@ static void _librust_qstrs(void) { MP_QSTR_button_event; MP_QSTR_cancel_arrow; MP_QSTR_case_sensitive; + MP_QSTR_chunkify; MP_QSTR_confirm_action; MP_QSTR_confirm_address; MP_QSTR_confirm_backup; diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 954cbcd066..d713e9b0e3 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -52,6 +52,40 @@ pub struct TextLayout { pub continues_from_prev_page: bool, } +/// Configuration for chunkifying the text into smaller parts. +#[derive(Copy, Clone)] +pub struct Chunks { + /// How many characters will be grouped in one chunk. + pub chunk_size: usize, + /// How big will be the space between chunks (in pixels). + pub x_offset: i16, + /// Optional characters that are wider and should be accounted for + pub wider_chars: Option<&'static str>, +} + +impl Chunks { + pub const fn new(chunk_size: usize, x_offset: i16) -> Self { + Chunks { + chunk_size, + x_offset, + wider_chars: None, + } + } + + pub const fn with_wider_chars(mut self, wider_chars: &'static str) -> Self { + self.wider_chars = Some(wider_chars); + self + } + + pub fn is_char_wider(self, ch: char) -> bool { + if let Some(wider_chars) = self.wider_chars { + wider_chars.contains(ch) + } else { + false + } + } +} + #[derive(Copy, Clone)] pub struct TextStyle { /// Text font ID. @@ -76,6 +110,14 @@ pub struct TextStyle { pub line_breaking: LineBreaking, /// Specifies what to do at the end of the page. pub page_breaking: PageBreaking, + + /// Optionally chunkify all the text with a specified chunk + /// size and pixel offset for the next chunk. + pub chunks: Option, + + /// Optionally increase the vertical space between text lines + /// (can be even negative, in which case it will decrease it). + pub line_spacing: i16, } impl TextStyle { @@ -96,6 +138,8 @@ impl TextStyle { prev_page_ellipsis_icon: None, line_breaking: LineBreaking::BreakAtWhitespace, page_breaking: PageBreaking::CutAndInsertEllipsis, + chunks: None, + line_spacing: 0, } } @@ -121,6 +165,18 @@ impl TextStyle { self } + /// Adding optional chunkification to the text. + pub const fn with_chunks(mut self, chunks: Chunks) -> Self { + self.chunks = Some(chunks); + self + } + + /// Adding optional change of vertical line spacing. + pub const fn with_line_spacing(mut self, line_spacing: i16) -> Self { + self.line_spacing = line_spacing; + self + } + fn ellipsis_width(&self) -> i16 { if let Some((icon, margin)) = self.ellipsis_icon { icon.toif.width() + margin @@ -220,12 +276,13 @@ impl TextLayout { }; let remaining_width = self.bounds.x1 - cursor.x; - let span = Span::fit_horizontally( + let mut span = Span::fit_horizontally( remaining_text, remaining_width, self.style.text_font, self.style.line_breaking, line_ending_space, + self.style.chunks, ); cursor.x += match self.align { @@ -251,6 +308,9 @@ impl TextLayout { if span.advance.y > 0 { // We're advancing to the next line. + // Possibly making a bigger/smaller vertical jump + span.advance.y += self.style.line_spacing; + // Check if we should be appending a hyphen at this point. if span.insert_hyphen_before_line_break { sink.hyphen(*cursor, self); @@ -488,6 +548,7 @@ impl Span { text_font: impl GlyphMetrics, breaking: LineBreaking, line_ending_space: i16, + chunks: Option, ) -> Self { const ASCII_LF: char = '\n'; const ASCII_CR: char = '\r'; @@ -537,6 +598,7 @@ impl Span { let mut span_width = 0; let mut found_any_whitespace = false; + let mut chunks_wider_chars = 0; let mut char_indices_iter = text.char_indices().peekable(); // Iterating manually because we need a reference to the iterator inside the @@ -544,6 +606,22 @@ impl Span { while let Some((i, ch)) = char_indices_iter.next() { let char_width = text_font.char_width(ch); + // When there is a set chunk size and we reach it, + // adjust the line advances and return the line. + if let Some(chunkify_config) = chunks { + if i == chunkify_config.chunk_size { + line.advance.y = 0; + // Decreasing the offset for each wider character in the chunk + line.advance.x += chunkify_config.x_offset - chunks_wider_chars; + return line; + } else { + // Counting all the wider characters in the chunk + if chunkify_config.is_char_wider(ch) { + chunks_wider_chars += 1; + } + } + } + // Consider if we could be breaking the line at this position. if is_whitespace(ch) && span_width + complete_word_end_width <= max_width { // Break before the whitespace, without hyphen. @@ -679,6 +757,7 @@ mod tests { FIXED_FONT, LineBreaking::BreakAtWhitespace, 0, + None, ); spans.push(( &remaining_text[..span.length], diff --git a/core/embed/rust/src/ui/component/text/op.rs b/core/embed/rust/src/ui/component/text/op.rs index 4a681ee902..a008100b69 100644 --- a/core/embed/rust/src/ui/component/text/op.rs +++ b/core/embed/rust/src/ui/component/text/op.rs @@ -8,7 +8,7 @@ use crate::{ }; use super::{ - layout::{LayoutFit, LayoutSink, TextLayout}, + layout::{Chunks, LayoutFit, LayoutSink, TextLayout}, LineBreaking, TextStyle, }; @@ -90,6 +90,12 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout { cursor.x += offset.x; cursor.y += offset.y; } + Op::Chunkify(chunks) => { + self.layout.style.chunks = chunks; + } + Op::LineSpacing(line_spacing) => { + self.layout.style.line_spacing = line_spacing; + } // Moving to the next page Op::NextPage => { // Pretending that nothing more fits on current page to force @@ -219,6 +225,14 @@ impl OpTextLayout { pub fn line_breaking(self, line_breaking: LineBreaking) -> Self { self.with_new_item(Op::LineBreaking(line_breaking)) } + + pub fn chunks(self, chunks: Option) -> Self { + self.with_new_item(Op::Chunkify(chunks)) + } + + pub fn line_spacing(self, spacing: i16) -> Self { + self.with_new_item(Op::LineSpacing(spacing)) + } } // Op-adding aggregation operations @@ -238,6 +252,14 @@ impl OpTextLayout { pub fn text_demibold(self, text: T) -> Self { self.font(Font::DEMIBOLD).text(text) } + + pub fn chunkify_text(self, chunks: Option<(Chunks, i16)>) -> Self { + if let Some(chunks) = chunks { + self.chunks(Some(chunks.0)).line_spacing(chunks.1) + } else { + self.chunks(None).line_spacing(0) + } + } } #[derive(Clone)] @@ -258,4 +280,8 @@ pub enum Op { CursorOffset(Offset), /// Force continuing on the next page. NextPage, + /// Render the following text in a chunkified way. None will disable that. + Chunkify(Option), + /// Change the line vertical line spacing. + LineSpacing(i16), } diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 6b80596fbf..59f4f4393a 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -22,7 +22,7 @@ use crate::{ }, TextStyle, }, - ComponentExt, FormattedText, LineBreaking, Timeout, + ComponentExt, FormattedText, Timeout, }, display, geometry, layout::{ @@ -557,6 +557,7 @@ extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut M let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?; let address_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_title)?.try_into()?; let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; let get_page = move |page_index| { // Showing two screens - the recipient address and summary confirmation @@ -567,11 +568,20 @@ extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut M let btn_actions = ButtonActions::cancel_none_next(); // Not putting hyphens in the address. // Potentially adding address label in different font. - let mut ops = OpTextLayout::new(theme::TEXT_MONO) - .line_breaking(LineBreaking::BreakWordsNoHyphen); + let mut ops = OpTextLayout::new(theme::TEXT_MONO_DATA); if !address_label.is_empty() { + // NOTE: need to explicitly turn off the chunkification before rendering the + // address label (for some reason it does not help to turn it off after + // rendering the chunks) + if chunkify { + ops = ops.chunkify_text(None); + } ops = ops.text_normal(address_label.clone()).newline(); } + if chunkify { + // Chunkifying the address into smaller pieces when requested + ops = ops.chunkify_text(Some((theme::MONO_CHUNKS, 2))); + } ops = ops.text_mono(address.clone()); let formatted = FormattedText::new(ops).vertically_centered(); Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone()) @@ -752,15 +762,20 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut 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_data)?.try_into()?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; let get_page = move |page_index| { assert!(page_index == 0); let btn_layout = ButtonLayout::cancel_armed_info("CONFIRM".into()); let btn_actions = ButtonActions::cancel_confirm_info(); - let ops = OpTextLayout::new(theme::TEXT_MONO) - .line_breaking(LineBreaking::BreakWordsNoHyphen) - .text_mono(address.clone()); + let style = if chunkify { + // Chunkifying the address into smaller pieces when requested + theme::TEXT_MONO_ADDRESS_CHUNKS + } else { + theme::TEXT_MONO_DATA + }; + let ops = OpTextLayout::new(style).text_mono(address.clone()); let formatted = FormattedText::new(ops).vertically_centered(); Page::new(btn_layout, btn_actions, formatted).with_title(title.clone()) }; @@ -1584,6 +1599,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// data: str, /// description: str | None, # unused on TR /// extra: str | None, # unused on TR + /// chunkify: bool = False, /// ) -> object: /// """Confirm address.""" Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(), @@ -1659,6 +1675,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// amount: str, /// address_title: str, /// amount_title: str, + /// chunkify: bool = False, /// ) -> object: /// """Confirm output.""" Qstr::MP_QSTR_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(), diff --git a/core/embed/rust/src/ui/model_tr/theme.rs b/core/embed/rust/src/ui/model_tr/theme.rs index 3da744edea..0dd5b16031 100644 --- a/core/embed/rust/src/ui/model_tr/theme.rs +++ b/core/embed/rust/src/ui/model_tr/theme.rs @@ -1,5 +1,8 @@ use crate::ui::{ - component::{text::TextStyle, LineBreaking, PageBreaking}, + component::{ + text::{layout::Chunks, TextStyle}, + LineBreaking, PageBreaking, + }, display::{toif::Icon, Color, Font}, geometry::Offset, }; @@ -35,6 +38,13 @@ pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, FG, FG) /// Mono data text does not have hyphens pub const TEXT_MONO_DATA: TextStyle = TEXT_MONO.with_line_breaking(LineBreaking::BreakWordsNoHyphen); +pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO_DATA + .with_chunks(MONO_CHUNKS) + .with_line_spacing(2) + .with_ellipsis_icon(ICON_NEXT_PAGE, -2); + +// Chunks for this model, with accounting for some wider characters in MONO font +pub const MONO_CHUNKS: Chunks = Chunks::new(4, 4).with_wider_chars("mMwW"); /// Convert Python-side numeric id to a `TextStyle`. pub fn textstyle_number(num: i32) -> &'static TextStyle { diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 4ef825d48d..fc9e602acf 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -496,6 +496,7 @@ struct ConfirmBlobParams { verb_cancel: Option, info_button: bool, hold: bool, + chunkify: bool, } impl ConfirmBlobParams { @@ -517,6 +518,7 @@ impl ConfirmBlobParams { verb_cancel, info_button: false, hold, + chunkify: false, } } @@ -535,6 +537,11 @@ impl ConfirmBlobParams { self } + fn with_chunkify(mut self, chunkify: bool) -> Self { + self.chunkify = chunkify; + self + } + fn into_layout(self) -> Result { let paragraphs = ConfirmBlob { description: self.description.unwrap_or_else(StrBuffer::empty), @@ -542,7 +549,11 @@ impl ConfirmBlobParams { data: self.data.try_into()?, description_font: &theme::TEXT_NORMAL, extra_font: &theme::TEXT_DEMIBOLD, - data_font: &theme::TEXT_MONO, + data_font: if self.chunkify { + &theme::TEXT_MONO_ADDRESS_CHUNKS + } else { + &theme::TEXT_MONO + }, } .into_paragraphs(); @@ -611,6 +622,21 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; let extra: Option = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?; let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; + + let data_style = if chunkify { + // Longer addresses have smaller x_offset so they fit even with scrollbar + // (as they will be shown on more than one page) + const FITS_ON_ONE_PAGE: usize = 16 * 4; + let address: StrBuffer = data.try_into()?; + if address.len() <= FITS_ON_ONE_PAGE { + &theme::TEXT_MONO_ADDRESS_CHUNKS + } else { + &theme::TEXT_MONO_ADDRESS_CHUNKS_SMALLER_X_OFFSET + } + } else { + &theme::TEXT_MONO + }; let paragraphs = ConfirmBlob { description: description.unwrap_or_else(StrBuffer::empty), @@ -618,7 +644,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut data: data.try_into()?, description_font: &theme::TEXT_NORMAL, extra_font: &theme::TEXT_DEMIBOLD, - data_font: &theme::TEXT_MONO, + data_font: data_style, } .into_paragraphs(); @@ -805,10 +831,12 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma .unwrap_or_else(|_| Obj::const_none()) .try_into_option()?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; ConfirmBlobParams::new(title, value, description, verb, verb_cancel, hold) .with_subtitle(subtitle) .with_info_button(info_button) + .with_chunkify(chunkify) .into_layout() }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1680,6 +1708,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// data: str | bytes, /// description: str | None, /// extra: str | None, + /// chunkify: bool = False, /// ) -> object: /// """Confirm address. Similar to `confirm_blob` but has corner info button /// and allows left swipe which does the same thing as the button.""" @@ -1734,6 +1763,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// verb_cancel: str | None = None, /// info_button: bool = False, /// hold: bool = False, + /// chunkify: bool = False, /// ) -> object: /// """Confirm value. Merge of confirm_total and confirm_output.""" Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(), diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 9abd850587..d3ca2c4695 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -2,7 +2,7 @@ use crate::{ time::Duration, ui::{ component::{ - text::{LineBreaking, PageBreaking, TextStyle}, + text::{layout::Chunks, LineBreaking, PageBreaking, TextStyle}, FixedHeightBar, }, display::{Color, Font, Icon}, @@ -517,6 +517,18 @@ pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT, .with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth) .with_ellipsis_icon(ICON_PAGE_NEXT, 0) .with_prev_page_icon(ICON_PAGE_PREV, 0); +/// Makes sure that the displayed text (usually address) will get divided into +/// smaller chunks. +pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO + .with_chunks(Chunks::new(4, 9)) + .with_line_spacing(5); +/// Smaller horizontal chunk offset, used e.g. for long Cardano addresses. +/// Also moving the next page ellipsis to the left (as there is a space on the +/// left). +pub const TEXT_MONO_ADDRESS_CHUNKS_SMALLER_X_OFFSET: TextStyle = TEXT_MONO + .with_chunks(Chunks::new(4, 7)) + .with_line_spacing(5) + .with_ellipsis_icon(ICON_PAGE_NEXT, -12); /// Convert Python-side numeric id to a `TextStyle`. pub fn textstyle_number(num: i32) -> &'static TextStyle { diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 506f278fd2..a6c89546ad 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -50,6 +50,7 @@ def confirm_address( data: str, description: str | None, # unused on TR extra: str | None, # unused on TR + chunkify: bool = False, ) -> object: """Confirm address.""" @@ -132,6 +133,7 @@ def confirm_output( amount: str, address_title: str, amount_title: str, + chunkify: bool = False, ) -> object: """Confirm output.""" @@ -496,6 +498,7 @@ def confirm_address( data: str | bytes, description: str | None, extra: str | None, + chunkify: bool = False, ) -> object: """Confirm address. Similar to `confirm_blob` but has corner info button and allows left swipe which does the same thing as the button.""" @@ -555,6 +558,7 @@ def confirm_value( verb_cancel: str | None = None, info_button: bool = False, hold: bool = False, + chunkify: bool = False, ) -> object: """Confirm value. Merge of confirm_total and confirm_output.""" diff --git a/core/src/apps/binance/get_address.py b/core/src/apps/binance/get_address.py index 5f9ad6254d..2cbe0dca94 100644 --- a/core/src/apps/binance/get_address.py +++ b/core/src/apps/binance/get_address.py @@ -26,6 +26,8 @@ async def get_address(msg: BinanceGetAddress, keychain: Keychain) -> BinanceAddr pubkey = node.public_key() address = address_from_public_key(pubkey, HRP) if msg.show_display: - await show_address(address, path=paths.address_n_to_str(address_n)) + await show_address( + address, path=paths.address_n_to_str(address_n), chunkify=bool(msg.chunkify) + ) return BinanceAddress(address=address) diff --git a/core/src/apps/binance/layout.py b/core/src/apps/binance/layout.py index 95eea31cf3..115f59eeb9 100644 --- a/core/src/apps/binance/layout.py +++ b/core/src/apps/binance/layout.py @@ -34,21 +34,18 @@ async def require_confirm_transfer(msg: BinanceTransferMsg) -> None: for txoutput in msg.outputs: make_input_output_pages(txoutput, "Confirm output") - await _confirm_transfer(items) + await _confirm_transfer(items, chunkify=bool(msg.chunkify)) -async def _confirm_transfer(inputs_outputs: Sequence[tuple[str, str, str]]) -> None: +async def _confirm_transfer( + inputs_outputs: Sequence[tuple[str, str, str]], chunkify: bool +) -> None: from trezor.ui.layouts import confirm_output for index, (title, amount, address) in enumerate(inputs_outputs): # Having hold=True on the last item hold = index == len(inputs_outputs) - 1 - await confirm_output( - address, - amount, - title, - hold=hold, - ) + await confirm_output(address, amount, title, hold=hold, chunkify=chunkify) async def require_confirm_cancel(msg: BinanceCancelMsg) -> None: diff --git a/core/src/apps/bitcoin/get_address.py b/core/src/apps/bitcoin/get_address.py index 5b782a3f68..408b6c0d06 100644 --- a/core/src/apps/bitcoin/get_address.py +++ b/core/src/apps/bitcoin/get_address.py @@ -113,6 +113,7 @@ async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Ad multisig_index=multisig_index, xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes), account=f"Multisig {multisig.m} of {len(pubnodes)}", + chunkify=bool(msg.chunkify), ) else: account_name = address_n_to_name(coin, address_n, script_type) @@ -128,6 +129,7 @@ async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Ad case_sensitive=address_case_sensitive, path=path, account=account, + chunkify=bool(msg.chunkify), ) return Address(address=address, mac=mac) diff --git a/core/src/apps/bitcoin/keychain.py b/core/src/apps/bitcoin/keychain.py index a4d9a720bf..d6a99fd362 100644 --- a/core/src/apps/bitcoin/keychain.py +++ b/core/src/apps/bitcoin/keychain.py @@ -356,6 +356,7 @@ class AccountType: coin: coininfo.CoinInfo, address_n: Bip32Path, script_type: InputScriptType | None, + show_account_str: bool, ) -> str | None: pattern = self.pattern if self.account_level: @@ -373,6 +374,8 @@ class AccountType: return None name = self.account_name + if show_account_str: + name = f"{self.account_name} account" account_pos = pattern.find("/account'") if account_pos >= 0: i = pattern.count("/", 0, account_pos) @@ -387,6 +390,7 @@ def address_n_to_name( address_n: Bip32Path, script_type: InputScriptType | None = None, account_level: bool = False, + show_account_str: bool = False, ) -> str | None: ACCOUNT_TYPES = ( AccountType( @@ -446,7 +450,7 @@ def address_n_to_name( ) for account in ACCOUNT_TYPES: - name = account.get_name(coin, address_n, script_type) + name = account.get_name(coin, address_n, script_type, show_account_str) if name: return name diff --git a/core/src/apps/bitcoin/sign_tx/approvers.py b/core/src/apps/bitcoin/sign_tx/approvers.py index 23d853f7be..27ac1cebcc 100644 --- a/core/src/apps/bitcoin/sign_tx/approvers.py +++ b/core/src/apps/bitcoin/sign_tx/approvers.py @@ -144,6 +144,7 @@ class BasicApprover(Approver): super().__init__(tx, coin) self.change_count = 0 # the number of change-outputs self.foreign_address_confirmed = False + self.chunkify = bool(tx.chunkify) async def add_internal_input(self, txi: TxInput, node: bip32.HDNode) -> None: if not validate_path_against_script_type(self.coin, txi): @@ -224,7 +225,11 @@ class BasicApprover(Approver): # Ask user to confirm output, unless it is part of a payment # request, which gets confirmed separately. await helpers.confirm_output( - txo, self.coin, self.amount_unit, self.external_output_index + txo, + self.coin, + self.amount_unit, + self.external_output_index, + self.chunkify, ) self.external_output_index += 1 diff --git a/core/src/apps/bitcoin/sign_tx/helpers.py b/core/src/apps/bitcoin/sign_tx/helpers.py index 51acb56e71..5ee9cdee2e 100644 --- a/core/src/apps/bitcoin/sign_tx/helpers.py +++ b/core/src/apps/bitcoin/sign_tx/helpers.py @@ -44,11 +44,13 @@ class UiConfirmOutput(UiConfirm): coin: CoinInfo, amount_unit: AmountUnit, output_index: int, + chunkify: bool, ): self.output = output self.coin = coin self.amount_unit = amount_unit self.output_index = output_index + self.chunkify = chunkify def confirm_dialog(self) -> Awaitable[Any]: return layout.confirm_output( @@ -56,6 +58,7 @@ class UiConfirmOutput(UiConfirm): self.coin, self.amount_unit, self.output_index, + self.chunkify, ) @@ -238,8 +241,8 @@ class UiConfirmMultipleAccounts(UiConfirm): return layout.confirm_multiple_accounts() -def confirm_output(output: TxOutput, coin: CoinInfo, amount_unit: AmountUnit, output_index: int) -> Awaitable[None]: # type: ignore [awaitable-is-generator] - return (yield UiConfirmOutput(output, coin, amount_unit, output_index)) +def confirm_output(output: TxOutput, coin: CoinInfo, amount_unit: AmountUnit, output_index: int, chunkify: bool) -> Awaitable[None]: # type: ignore [awaitable-is-generator] + return (yield UiConfirmOutput(output, coin, amount_unit, output_index, chunkify)) def confirm_decred_sstx_submission(output: TxOutput, coin: CoinInfo, amount_unit: AmountUnit) -> Awaitable[None]: # type: ignore [awaitable-is-generator] diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py index 59dc93d7cf..4323cc410f 100644 --- a/core/src/apps/bitcoin/sign_tx/layout.py +++ b/core/src/apps/bitcoin/sign_tx/layout.py @@ -62,6 +62,7 @@ async def confirm_output( coin: CoinInfo, amount_unit: AmountUnit, output_index: int, + chunkify: bool, ) -> None: from trezor.enums import OutputScriptType @@ -97,9 +98,18 @@ async def confirm_output( address_label = None if output.address_n and not output.multisig: + from trezor import utils + + # Showing the account string only for T2B1 model + show_account_str = utils.INTERNAL_MODEL == "T2B1" script_type = CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES[output.script_type] address_label = ( - address_n_to_name(coin, output.address_n, script_type) + address_n_to_name( + coin, + output.address_n, + script_type, + show_account_str=show_account_str, + ) or f"address path {address_n_to_str(output.address_n)}" ) @@ -109,6 +119,7 @@ async def confirm_output( title=title, address_label=address_label, output_index=output_index, + chunkify=chunkify, ) await layout @@ -147,7 +158,7 @@ async def confirm_payment_request( ) -> Any: from trezor import wire - memo_texts = [] + memo_texts: list[str] = [] for m in msg.memos: if m.text_memo is not None: memo_texts.append(m.text_memo.text) diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index c5a627dca2..e1685aff0d 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -39,6 +39,8 @@ async def get_address( Credential.payment_credential(address_parameters), Credential.stake_credential(address_parameters), ) - await show_cardano_address(address_parameters, address, msg.protocol_magic) + await show_cardano_address( + address_parameters, address, msg.protocol_magic, chunkify=bool(msg.chunkify) + ) return CardanoAddress(address=address) diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index b93710b985..aa95efa8eb 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -210,6 +210,7 @@ async def confirm_sending( to: str, output_type: Literal["address", "change", "collateral-return"], network_id: int, + chunkify: bool, ) -> None: if output_type == "address": title = "Sending" @@ -225,6 +226,7 @@ async def confirm_sending( format_coin_amount(ada_amount, network_id), title, br_code=ButtonRequestType.Other, + chunkify=chunkify, ) @@ -898,6 +900,7 @@ async def show_cardano_address( address_parameters: messages.CardanoAddressParametersType, address: str, protocol_magic: int, + chunkify: bool, ) -> None: CAT = CardanoAddressType # local_cache_global @@ -925,4 +928,5 @@ async def show_cardano_address( path=path, account=account, network=network_name, + chunkify=chunkify, ) diff --git a/core/src/apps/cardano/sign_tx/signer.py b/core/src/apps/cardano/sign_tx/signer.py index f8b22bc34a..9627c87365 100644 --- a/core/src/apps/cardano/sign_tx/signer.py +++ b/core/src/apps/cardano/sign_tx/signer.py @@ -366,6 +366,7 @@ class Signer: address, "change" if self._is_change_output(output) else "address", self.msg.network_id, + chunkify=bool(self.msg.chunkify), ) async def _show_output_credentials( @@ -1043,6 +1044,7 @@ class Signer: address, "collateral-return", self.msg.network_id, + chunkify=bool(self.msg.chunkify), ) def _should_show_collateral_return_init(self, output: CardanoTxOutput) -> bool: diff --git a/core/src/apps/ethereum/get_address.py b/core/src/apps/ethereum/get_address.py index 6a8c12e2cc..33de65fa39 100644 --- a/core/src/apps/ethereum/get_address.py +++ b/core/src/apps/ethereum/get_address.py @@ -32,6 +32,8 @@ async def get_address( address = address_from_bytes(node.ethereum_pubkeyhash(), defs.network) if msg.show_display: - await show_address(address, path=paths.address_n_to_str(address_n)) + await show_address( + address, path=paths.address_n_to_str(address_n), chunkify=bool(msg.chunkify) + ) return EthereumAddress(address=address) diff --git a/core/src/apps/monero/get_address.py b/core/src/apps/monero/get_address.py index be7970f5a9..49ab3f28d7 100644 --- a/core/src/apps/monero/get_address.py +++ b/core/src/apps/monero/get_address.py @@ -69,6 +69,7 @@ async def get_address(msg: MoneroGetAddress, keychain: Keychain) -> MoneroAddres addr, address_qr="monero:" + addr, path=paths.address_n_to_str(msg.address_n), + chunkify=bool(msg.chunkify), ) return MoneroAddress(address=addr.encode()) diff --git a/core/src/apps/monero/layout.py b/core/src/apps/monero/layout.py index 31d0c20fff..a0c2e01e7f 100644 --- a/core/src/apps/monero/layout.py +++ b/core/src/apps/monero/layout.py @@ -126,7 +126,9 @@ async def require_confirm_transaction( cur_payment = payment_id else: cur_payment = None - await _require_confirm_output(dst, network_type, cur_payment) + await _require_confirm_output( + dst, network_type, cur_payment, chunkify=bool(tsx_data.chunkify) + ) if ( payment_id @@ -143,6 +145,7 @@ async def _require_confirm_output( dst: MoneroTransactionDestinationEntry, network_type: MoneroNetworkType, payment_id: bytes | None, + chunkify: bool, ) -> None: """ Single transaction destination confirmation @@ -161,6 +164,7 @@ async def _require_confirm_output( addr, _format_amount(dst.amount), br_code=BRT_SignTx, + chunkify=chunkify, ) diff --git a/core/src/apps/nem/get_address.py b/core/src/apps/nem/get_address.py index 4686cbd6cd..d0c9061ffe 100644 --- a/core/src/apps/nem/get_address.py +++ b/core/src/apps/nem/get_address.py @@ -35,6 +35,7 @@ async def get_address(msg: NEMGetAddress, keychain: Keychain) -> NEMAddress: case_sensitive=False, path=address_n_to_str(address_n), network=get_network_str(network), + chunkify=bool(msg.chunkify), ) return NEMAddress(address=address) diff --git a/core/src/apps/nem/sign_tx.py b/core/src/apps/nem/sign_tx.py index 4cb76c13b0..a4216af366 100644 --- a/core/src/apps/nem/sign_tx.py +++ b/core/src/apps/nem/sign_tx.py @@ -46,7 +46,9 @@ async def sign_tx(msg: NEMSignTx, keychain: Keychain) -> NEMSignedTx: common = transaction if msg.transfer: - tx = await transfer.transfer(public_key, common, msg.transfer, node) + tx = await transfer.transfer( + public_key, common, msg.transfer, node, chunkify=bool(msg.chunkify) + ) elif msg.provision_namespace: tx = await namespace.namespace(public_key, common, msg.provision_namespace) elif msg.mosaic_creation: diff --git a/core/src/apps/nem/transfer/__init__.py b/core/src/apps/nem/transfer/__init__.py index 2e576fce58..c6a95d39a7 100644 --- a/core/src/apps/nem/transfer/__init__.py +++ b/core/src/apps/nem/transfer/__init__.py @@ -12,11 +12,12 @@ async def transfer( common: NEMTransactionCommon, transfer: NEMTransfer, node: bip32.HDNode, + chunkify: bool, ) -> bytes: transfer.mosaics = serialize.canonicalize_mosaics(transfer.mosaics) payload, encrypted = serialize.get_transfer_payload(transfer, node) - await layout.ask_transfer(common, transfer, encrypted) + await layout.ask_transfer(common, transfer, encrypted, chunkify) w = serialize.serialize_transfer(common, transfer, public_key, payload, encrypted) for mosaic in transfer.mosaics: diff --git a/core/src/apps/nem/transfer/layout.py b/core/src/apps/nem/transfer/layout.py index 5828e8cb38..d615c44265 100644 --- a/core/src/apps/nem/transfer/layout.py +++ b/core/src/apps/nem/transfer/layout.py @@ -20,6 +20,7 @@ async def ask_transfer( common: NEMTransactionCommon, transfer: NEMTransfer, encrypted: bool, + chunkify: bool, ) -> None: from trezor.ui.layouts import confirm_output, confirm_text @@ -42,6 +43,7 @@ async def ask_transfer( await confirm_output( transfer.recipient, f"{format_amount(_get_xem_amount(transfer), NEM_MAX_DIVISIBILITY)} XEM", + chunkify=chunkify, ) await require_confirm_final(common.fee) diff --git a/core/src/apps/ripple/get_address.py b/core/src/apps/ripple/get_address.py index c0c96d1cd7..b21d9659f2 100644 --- a/core/src/apps/ripple/get_address.py +++ b/core/src/apps/ripple/get_address.py @@ -25,6 +25,10 @@ async def get_address(msg: RippleGetAddress, keychain: Keychain) -> RippleAddres address = address_from_public_key(pubkey) if msg.show_display: - await show_address(address, path=paths.address_n_to_str(msg.address_n)) + await show_address( + address, + path=paths.address_n_to_str(msg.address_n), + chunkify=bool(msg.chunkify), + ) return RippleAddress(address=address) diff --git a/core/src/apps/ripple/layout.py b/core/src/apps/ripple/layout.py index b9f2ebc6de..f91934d2a1 100644 --- a/core/src/apps/ripple/layout.py +++ b/core/src/apps/ripple/layout.py @@ -22,7 +22,7 @@ async def require_confirm_destination_tag(tag: int) -> None: ) -async def require_confirm_tx(to: str, value: int) -> None: +async def require_confirm_tx(to: str, value: int, chunkify: bool = False) -> None: from trezor.ui.layouts import confirm_output - await confirm_output(to, format_amount(value, DECIMALS) + " XRP") + await confirm_output(to, format_amount(value, DECIMALS) + " XRP", chunkify=chunkify) diff --git a/core/src/apps/ripple/sign_tx.py b/core/src/apps/ripple/sign_tx.py index f9de3eb8ae..c8957bec4d 100644 --- a/core/src/apps/ripple/sign_tx.py +++ b/core/src/apps/ripple/sign_tx.py @@ -47,7 +47,9 @@ async def sign_tx(msg: RippleSignTx, keychain: Keychain) -> RippleSignedTx: if payment.destination_tag is not None: await layout.require_confirm_destination_tag(payment.destination_tag) - await layout.require_confirm_tx(payment.destination, payment.amount) + await layout.require_confirm_tx( + payment.destination, payment.amount, chunkify=bool(msg.chunkify) + ) await layout.require_confirm_total(payment.amount + msg.fee, msg.fee) # Signs and encodes signature into DER format diff --git a/core/src/apps/stellar/get_address.py b/core/src/apps/stellar/get_address.py index cc9fea05dd..6d48b23108 100644 --- a/core/src/apps/stellar/get_address.py +++ b/core/src/apps/stellar/get_address.py @@ -25,6 +25,8 @@ async def get_address(msg: StellarGetAddress, keychain: Keychain) -> StellarAddr if msg.show_display: path = paths.address_n_to_str(msg.address_n) - await show_address(address, case_sensitive=False, path=path) + await show_address( + address, case_sensitive=False, path=path, chunkify=bool(msg.chunkify) + ) return StellarAddress(address=address) diff --git a/core/src/apps/tezos/get_address.py b/core/src/apps/tezos/get_address.py index 1ab6407873..d7fd52d277 100644 --- a/core/src/apps/tezos/get_address.py +++ b/core/src/apps/tezos/get_address.py @@ -29,6 +29,10 @@ async def get_address(msg: TezosGetAddress, keychain: Keychain) -> TezosAddress: address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX) if msg.show_display: - await show_address(address, path=paths.address_n_to_str(msg.address_n)) + await show_address( + address, + path=paths.address_n_to_str(msg.address_n), + chunkify=bool(msg.chunkify), + ) return TezosAddress(address=address) diff --git a/core/src/apps/tezos/layout.py b/core/src/apps/tezos/layout.py index 1589581c9f..78f7e2aee0 100644 --- a/core/src/apps/tezos/layout.py +++ b/core/src/apps/tezos/layout.py @@ -4,13 +4,14 @@ from trezor.ui.layouts import confirm_address, confirm_metadata, confirm_propert BR_SIGN_TX = ButtonRequestType.SignTx # global_import_cache -async def require_confirm_tx(to: str, value: int) -> None: +async def require_confirm_tx(to: str, value: int, chunkify: bool = False) -> None: from trezor.ui.layouts import confirm_output await confirm_output( to, format_tezos_amount(value), br_code=BR_SIGN_TX, + chunkify=chunkify, ) diff --git a/core/src/apps/tezos/sign_tx.py b/core/src/apps/tezos/sign_tx.py index c0d172c62b..124402161c 100644 --- a/core/src/apps/tezos/sign_tx.py +++ b/core/src/apps/tezos/sign_tx.py @@ -71,12 +71,16 @@ async def sign_tx(msg: TezosSignTx, keychain: Keychain) -> TezosSignedTx: # operation to transfer tokens from a smart contract to an implicit account or a smart contract elif transfer is not None: to = _get_address_from_contract(transfer.destination) - await layout.require_confirm_tx(to, transfer.amount) + await layout.require_confirm_tx( + to, transfer.amount, chunkify=bool(msg.chunkify) + ) await layout.require_confirm_fee(transfer.amount, fee) else: # transactions from an implicit account to = _get_address_from_contract(transaction.destination) - await layout.require_confirm_tx(to, transaction.amount) + await layout.require_confirm_tx( + to, transaction.amount, chunkify=bool(msg.chunkify) + ) await layout.require_confirm_fee(transaction.amount, fee) elif origination is not None: diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 3638f927cc..4b0135b976 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -466,6 +466,7 @@ async def show_address( mismatch_title: str = "ADDRESS MISMATCH?", br_type: str = "show_address", br_code: ButtonRequestType = ButtonRequestType.Address, + chunkify: bool = False, ) -> None: send_button_request = True if title is None: @@ -482,6 +483,7 @@ async def show_address( data=address, description="", # unused on TR extra=None, # unused on TR + chunkify=chunkify, ) ) if send_button_request: @@ -550,6 +552,7 @@ def show_pubkey( br_type=br_type, br_code=ButtonRequestType.PublicKey, mismatch_title=mismatch_title, + chunkify=False, ) @@ -652,6 +655,7 @@ async def confirm_output( br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, address_label: str | None = None, output_index: int | None = None, + chunkify: bool = False, ) -> None: address_title = ( "RECIPIENT" if output_index is None else f"RECIPIENT #{output_index + 1}" @@ -667,6 +671,7 @@ async def confirm_output( address_title=address_title, amount_title=amount_title, amount=amount, + chunkify=chunkify, ) ), "confirm_output", diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 93dce7d109..acdfa987d3 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -417,6 +417,7 @@ async def show_address( mismatch_title: str = "Address mismatch?", br_type: str = "show_address", br_code: ButtonRequestType = ButtonRequestType.Address, + chunkify: bool = False, ) -> None: send_button_request = True if title is None: @@ -435,6 +436,7 @@ async def show_address( data=address, description=network or "", extra=None, + chunkify=chunkify, ) ) if send_button_request: @@ -500,6 +502,7 @@ def show_pubkey( br_type=br_type, br_code=ButtonRequestType.PublicKey, mismatch_title=mismatch_title, + chunkify=False, ) @@ -577,6 +580,7 @@ async def confirm_output( br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, address_label: str | None = None, output_index: int | None = None, + chunkify: bool = False, ) -> None: if title is not None: if title.upper().startswith("CONFIRM "): @@ -601,6 +605,7 @@ async def confirm_output( verb="CONTINUE", hold=False, info_button=False, + chunkify=chunkify, ) ), "confirm_output",