diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index c6bdb93a9a..735d058987 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -16,10 +16,12 @@ static void _librust_qstrs(void) { MP_QSTR___dict__; MP_QSTR___name__; MP_QSTR_account; + MP_QSTR_account_label; MP_QSTR_accounts; MP_QSTR_action; MP_QSTR_active; MP_QSTR_address; + MP_QSTR_address_label; MP_QSTR_address_title; MP_QSTR_allow_cancel; MP_QSTR_amount; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 0ef823224b..eb0c05f776 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -522,6 +522,7 @@ extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs: extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; + let address_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_label)?.try_into()?; 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 address_title_clone = address_title.clone(); @@ -532,23 +533,27 @@ extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut M match page_index { 0 => { // RECIPIENT + address - let btn_layout = ButtonLayout::cancel_none_text("CONFIRM".into()); + let btn_layout = ButtonLayout::cancel_none_text("CONTINUE".into()); let btn_actions = ButtonActions::cancel_none_next(); - // Not putting hyphens in the address - let ops = OpTextLayout::new(theme::TEXT_MONO) - .line_breaking(LineBreaking::BreakWordsNoHyphen) - .text_mono(address.clone()); - let formatted = FormattedText::new(ops); + // 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); + if !address_label.is_empty() { + ops = ops.text_normal(address_label.clone()).newline(); + } + ops = ops.text_mono(address.clone()); + let formatted = + FormattedText::new(ops).vertically_aligned(geometry::Alignment::Center); Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone()) } 1 => { // AMOUNT + amount let btn_layout = ButtonLayout::up_arrow_none_text("CONFIRM".into()); let btn_actions = ButtonActions::prev_none_confirm(); - let ops = OpTextLayout::new(theme::TEXT_MONO) - .newline() - .text_mono(amount.clone()); - let formatted = FormattedText::new(ops); + let ops = OpTextLayout::new(theme::TEXT_MONO).text_mono(amount.clone()); + let formatted = + FormattedText::new(ops).vertically_aligned(geometry::Alignment::Center); Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone()) } _ => unreachable!(), @@ -569,34 +574,75 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma let fee_rate_amount: Option = kwargs .get(Qstr::MP_QSTR_fee_rate_amount)? .try_into_option()?; + let account_label: Option = + kwargs.get(Qstr::MP_QSTR_account_label)?.try_into_option()?; let total_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_label)?.try_into()?; let fee_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_label)?.try_into()?; let get_page = move |page_index| { - // Total amount + fee - assert!(page_index == 0); + match page_index { + 0 => { + // Total amount + fee + let btn_layout = ButtonLayout::cancel_armed_info("CONFIRM".into()); + let btn_actions = ButtonActions::cancel_confirm_next(); - let btn_layout = ButtonLayout::cancel_none_htc("HOLD TO CONFIRM".into()); - let btn_actions = ButtonActions::cancel_none_confirm(); + let ops = OpTextLayout::new(theme::TEXT_MONO) + .text_bold(total_label.clone()) + .newline() + .text_mono(total_amount.clone()) + .newline() + .newline() + .text_bold(fee_label.clone()) + .newline() + .text_mono(fee_amount.clone()); - let mut ops = OpTextLayout::new(theme::TEXT_MONO) - .text_bold(total_label.clone()) - .newline() - .text_mono(total_amount.clone()) - .newline() - .text_bold(fee_label.clone()) - .newline() - .text_mono(fee_amount.clone()); + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + } + 1 => { + // Fee rate info + let btn_layout = ButtonLayout::arrow_none_arrow(); + let btn_actions = ButtonActions::prev_none_next(); - // Fee rate amount might not be there - if let Some(fee_rate_amount) = fee_rate_amount.clone() { - ops = ops.newline().text_mono(fee_rate_amount) + let fee_rate_amount = fee_rate_amount.clone().unwrap_or_default(); + + let ops = OpTextLayout::new(theme::TEXT_MONO) + .text_bold("FEE INFORMATION".into()) + .newline() + .newline() + .newline_half() + .text_bold("Fee rate:".into()) + .newline() + .text_mono(fee_rate_amount); + + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + } + 2 => { + // Wallet and account info + let btn_layout = ButtonLayout::arrow_none_none(); + let btn_actions = ButtonActions::prev_none_none(); + + let account_label = account_label.clone().unwrap_or_default(); + + // TODO: include wallet info when available + + let ops = OpTextLayout::new(theme::TEXT_MONO) + .text_bold("SENDING FROM".into()) + .newline() + .newline() + .newline_half() + .text_bold("Account:".into()) + .newline() + .text_mono(account_label); + + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + } + _ => unreachable!(), } - - let formatted = FormattedText::new(ops); - Page::new(btn_layout, btn_actions, formatted) }; - let pages = FlowPages::new(get_page, 1); + let pages = FlowPages::new(get_page, 3); let obj = LayoutObj::new(Flow::new(pages))?; Ok(obj.into()) @@ -612,7 +658,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut let get_page = move |page_index| { assert!(page_index == 0); - let btn_layout = ButtonLayout::cancel_armed_text("CONFIRM".into(), "i".into()); + 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) @@ -1354,6 +1400,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def confirm_output( /// *, /// address: str, + /// address_label: str, /// amount: str, /// address_title: str, /// amount_title: str, @@ -1366,6 +1413,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// total_amount: str, /// fee_amount: str, /// fee_rate_amount: str | None, + /// account_label: str | None, /// total_label: str, /// fee_label: str, /// ) -> object: diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index b4c4b4857f..1f67796f49 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -123,6 +123,7 @@ def confirm_modify_output( def confirm_output( *, address: str, + address_label: str, amount: str, address_title: str, amount_title: str, @@ -136,6 +137,7 @@ def confirm_total( total_amount: str, fee_amount: str, fee_rate_amount: str | None, + account_label: str | None, total_label: str, fee_label: str, ) -> object: diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 234bd84787..aacb484717 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -693,15 +693,13 @@ async def confirm_output( ) amount_title = "AMOUNT" if output_index is None else f"AMOUNT #{output_index + 1}" - # TODO: implement `hold` to be consistent with `TT`? - # TODO: incorporate label? - label = f" ({address_label})" if address_label else "" - await raise_if_not_confirmed( interact( ctx, RustLayout( trezorui2.confirm_output( address=address, + address_label=address_label or "", address_title=address_title, amount_title=amount_title, amount=amount, @@ -944,13 +942,11 @@ async def confirm_total( fee_rate_amount: str | None = None, title: str = "SENDING", total_label: str = "TOTAL AMOUNT", - fee_label: str = "INCLUDING FEE", + fee_label: str = "Including fee:", account_label: str | None = None, br_type: str = "confirm_total", br_code: ButtonRequestType = ButtonRequestType.SignTx, ) -> None: - # TODO: incorporate account_label - # f"From {account_label}\r\n{total_label}" if account_label else total_label, await raise_if_not_confirmed( interact( ctx, @@ -960,8 +956,9 @@ async def confirm_total( total_amount=total_amount, # type: ignore [No parameter named] fee_amount=fee_amount, # type: ignore [No parameter named] fee_rate_amount=fee_rate_amount, # type: ignore [No parameter named] + account_label=account_label, # type: ignore [No parameter named] total_label=total_label.upper(), # type: ignore [No parameter named] - fee_label=fee_label.upper(), # type: ignore [No parameter named] + fee_label=fee_label, # type: ignore [No parameter named] ) ), br_type, diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py index 31ee3b03ab..42056f3f18 100644 --- a/tests/click_tests/test_autolock.py +++ b/tests/click_tests/test_autolock.py @@ -104,7 +104,6 @@ def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"): layout = debug.click(buttons.OK, wait=True) assert "Total amount: 0.0039 BTC" in layout.text_content() elif debug.model == "R": - debug.press_right(wait=True) debug.press_right(wait=True) layout = debug.press_right(wait=True) assert "TOTAL AMOUNT 0.0039 BTC" in layout.text_content() @@ -165,7 +164,7 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa if debug.model == "T": debug.click(buttons.OK) elif debug.model == "R": - debug.press_right_htc(1200) + debug.press_middle() signatures, tx = device_handler.result() assert len(signatures) == 1 diff --git a/tests/device_tests/bitcoin/test_signtx.py b/tests/device_tests/bitcoin/test_signtx.py index 9f69b60078..e530d09f9c 100644 --- a/tests/device_tests/bitcoin/test_signtx.py +++ b/tests/device_tests/bitcoin/test_signtx.py @@ -27,6 +27,10 @@ from ...input_flows import ( InputFlowLockTimeBlockHeight, InputFlowLockTimeDatetime, InputFlowSignTxHighFee, + InputFlowSignTxInformation, + InputFlowSignTxInformationCancel, + InputFlowSignTxInformationMixed, + InputFlowSignTxInformationReplacement, ) from ...tx_cache import TxCache from .signtx import ( @@ -1524,7 +1528,6 @@ def test_lock_time_datetime(client: Client, lock_time_str: str): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") -@pytest.mark.skip_tr(reason="Not implemented yet") def test_information(client: Client): # input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5 @@ -1542,29 +1545,9 @@ def test_information(client: Client): script_type=messages.OutputScriptType.PAYTOADDRESS, ) - def input_flow(): - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - - yield # confirm transaction - client.debug.wait_layout() - client.debug.press_info() - - layout = client.debug.wait_layout() - content = layout.text_content().lower() - assert "sending from" in content - assert "legacy #6" in content - assert "fee rate" in content - assert "71.56 sat" in content - client.debug.click(CORNER_BUTTON, wait=True) - client.debug.press_yes() - with client: - client.set_input_flow(input_flow) + IF = InputFlowSignTxInformation(client) + client.set_input_flow(IF.get()) client.watch_layout(True) btc.sign_tx( @@ -1577,7 +1560,6 @@ def test_information(client: Client): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") -@pytest.mark.skip_tr(reason="Not implemented yet") def test_information_mixed(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/1h/0h/0/0"), # mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q @@ -1599,29 +1581,9 @@ def test_information_mixed(client: Client): script_type=messages.OutputScriptType.PAYTOADDRESS, ) - def input_flow(): - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - - yield # confirm transaction - client.debug.wait_layout() - client.debug.press_info() - - layout = client.debug.wait_layout() - content = layout.text_content().lower() - assert "sending from" in content - assert "multiple accounts" in content - assert "fee rate" in content - assert "18.33 sat" in content - client.debug.click(CORNER_BUTTON, wait=True) - client.debug.press_yes() - with client: - client.set_input_flow(input_flow) + IF = InputFlowSignTxInformationMixed(client) + client.set_input_flow(IF.get()) client.watch_layout(True) btc.sign_tx( @@ -1634,7 +1596,6 @@ def test_information_mixed(client: Client): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") -@pytest.mark.skip_tr(reason="Not implemented yet") def test_information_cancel(client: Client): # input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5 @@ -1652,24 +1613,9 @@ def test_information_cancel(client: Client): script_type=messages.OutputScriptType.PAYTOADDRESS, ) - def input_flow(): - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - yield # confirm output - client.debug.wait_layout() - client.debug.press_yes() - - yield # confirm transaction - client.debug.wait_layout() - client.debug.press_info() - - client.debug.wait_layout() - client.debug.click(CORNER_BUTTON, wait=True) - client.debug.press_no() - with client, pytest.raises(Cancelled): - client.set_input_flow(input_flow) + IF = InputFlowSignTxInformationCancel(client) + client.set_input_flow(IF.get()) client.watch_layout(True) btc.sign_tx( @@ -1682,7 +1628,6 @@ def test_information_cancel(client: Client): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") -@pytest.mark.skip_tr(reason="Input flow different on TR") def test_information_replacement(client: Client): # Use the change output and an external output to bump the fee. # Originally fee was 3780, now 108060 (94280 from change and 10000 from external). @@ -1715,25 +1660,9 @@ def test_information_replacement(client: Client): orig_index=0, ) - def input_flow(): - yield # confirm txid - client.debug.press_yes() - yield # confirm address - client.debug.press_yes() - # go back to address - client.debug.press_no() - # confirm address - client.debug.press_yes() - yield # confirm amount - client.debug.press_yes() - - yield # transaction summary, press info - client.debug.press_info(wait=True) - client.debug.click(CORNER_BUTTON, wait=True) - client.debug.press_yes() - with client: - client.set_input_flow(input_flow) + IF = InputFlowSignTxInformationReplacement(client) + client.set_input_flow(IF.get()) client.watch_layout(True) btc.sign_tx( diff --git a/tests/input_flows.py b/tests/input_flows.py index f45341502e..1e89e07ab1 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -446,6 +446,140 @@ class InputFlowSignTxHighFee(InputFlowBase): yield from self.go_through_all_screens(screens) +def sign_tx_go_to_info(client: Client) -> Generator[None, None, str]: + yield # confirm output + client.debug.wait_layout() + client.debug.press_yes() + yield # confirm output + client.debug.wait_layout() + client.debug.press_yes() + + yield # confirm transaction + client.debug.wait_layout() + client.debug.press_info() + + layout = client.debug.wait_layout() + content = layout.text_content().lower() + + client.debug.click(buttons.CORNER_BUTTON, wait=True) + + return content + + +def sign_tx_go_to_info_tr( + client: Client, +) -> Generator[None, None, str]: + yield # confirm output + client.debug.wait_layout() + client.debug.press_right() # CONTINUE + client.debug.wait_layout() + client.debug.press_right() # CONFIRM + + screen_texts: list[str] = [] + + yield # confirm total + layout = client.debug.press_right(wait=True) + screen_texts.append(layout.text_content()) + + layout = client.debug.press_right(wait=True) + screen_texts.append(layout.text_content()) + + client.debug.press_left() + client.debug.press_left() + + return "\n".join(screen_texts) + + +class InputFlowSignTxInformation(InputFlowBase): + def __init__(self, client: Client): + super().__init__(client) + + def assert_content(self, content: str) -> None: + assert "sending from" in content + assert "legacy #6" in content + assert "fee rate" in content + assert "71.56 sat" in content + + def input_flow_tt(self) -> GeneratorType: + content = yield from sign_tx_go_to_info(self.client) + self.assert_content(content) + self.client.debug.press_yes() + + def input_flow_tr(self) -> GeneratorType: + content = yield from sign_tx_go_to_info_tr(self.client) + self.assert_content(content.lower()) + self.client.debug.press_yes() + + +class InputFlowSignTxInformationMixed(InputFlowBase): + def __init__(self, client: Client): + super().__init__(client) + + def assert_content(self, content: str) -> None: + assert "sending from" in content + assert "multiple accounts" in content + assert "fee rate" in content + assert "18.33 sat" in content + + def input_flow_tt(self) -> GeneratorType: + content = yield from sign_tx_go_to_info(self.client) + self.assert_content(content) + self.client.debug.press_yes() + + def input_flow_tr(self) -> GeneratorType: + content = yield from sign_tx_go_to_info_tr(self.client) + self.assert_content(content.lower()) + self.client.debug.press_yes() + + +class InputFlowSignTxInformationCancel(InputFlowBase): + def __init__(self, client: Client): + super().__init__(client) + + def input_flow_tt(self) -> GeneratorType: + yield from sign_tx_go_to_info(self.client) + self.client.debug.press_no() + + def input_flow_tr(self) -> GeneratorType: + yield from sign_tx_go_to_info_tr(self.client) + self.client.debug.press_left() + + +class InputFlowSignTxInformationReplacement(InputFlowBase): + def __init__(self, client: Client): + super().__init__(client) + + def input_flow_tt(self) -> GeneratorType: + yield # confirm txid + self.client.debug.press_yes() + yield # confirm address + self.client.debug.press_yes() + # go back to address + self.client.debug.press_no() + # confirm address + self.client.debug.press_yes() + yield # confirm amount + self.client.debug.press_yes() + + yield # transaction summary, press info + self.client.debug.press_info(wait=True) + self.client.debug.click(buttons.CORNER_BUTTON, wait=True) + self.client.debug.press_yes() + + def input_flow_tr(self) -> GeneratorType: + yield # confirm txid + self.client.debug.press_right() + self.client.debug.press_right() + yield # confirm address + self.client.debug.press_right() + self.client.debug.press_right() + self.client.debug.press_right() + yield # confirm amount + self.client.debug.press_right() + self.client.debug.press_right() + self.client.debug.press_right() + + def lock_time_input_flow_tt( debug: DebugLink, layout_assert_func: Callable[[DebugLink], None],