From ddac852acf7c29d366e0c8b6b42b136e30371cd6 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Wed, 10 Mar 2021 10:52:26 +0100 Subject: [PATCH] refactor(core): convert apps.cardano to layouts --- core/src/apps/cardano/get_address.py | 27 +- core/src/apps/cardano/layout.py | 622 ++++++++++----------- core/src/apps/common/layout.py | 33 -- core/src/trezor/ui/components/tt/scroll.py | 17 +- core/src/trezor/ui/layouts/tt.py | 69 ++- tests/device_tests/cardano/test_sign_tx.py | 1 - 6 files changed, 378 insertions(+), 391 deletions(-) diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index 3891c64502..e1b8c064f2 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -1,8 +1,9 @@ from trezor import log, wire from trezor.messages import CardanoAddress +from trezor.ui.layouts import show_address from apps.common import paths -from apps.common.layout import address_n_to_str, show_qr +from apps.common.layout import address_n_to_str from . import seed from .address import derive_human_readable_address, validate_address_parameters @@ -10,7 +11,7 @@ from .helpers import protocol_magics, staking_use_cases from .helpers.paths import SCHEMA_ADDRESS from .helpers.utils import to_account_path from .layout import ( - show_address, + ADDRESS_TYPE_NAMES, show_warning_address_foreign_staking_key, show_warning_address_pointer, ) @@ -70,19 +71,15 @@ async def _display_address( if not protocol_magics.is_mainnet(protocol_magic): network_name = protocol_magics.to_ui_string(protocol_magic) - while True: - if await show_address( - ctx, - address, - address_parameters.address_type, - address_parameters.address_n, - network=network_name, - ): - break - if await show_qr( - ctx, address, desc=address_n_to_str(address_parameters.address_n) - ): - break + address_n = address_n_to_str(address_parameters.address_n) + await show_address( + ctx, + address=address, + title="%s address" % ADDRESS_TYPE_NAMES[address_parameters.address_type], + network=network_name, + address_extra=address_n, + title_qr=address_n, + ) async def _show_staking_warnings( diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index 0478d36ce0..1ac1add299 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -1,15 +1,15 @@ -import math from ubinascii import hexlify from trezor import ui from trezor.enums import ButtonRequestType, CardanoAddressType, CardanoCertificateType from trezor.strings import format_amount -from trezor.ui.components.tt.button import ButtonDefault -from trezor.ui.components.tt.scroll import Paginated -from trezor.ui.components.tt.text import Text -from trezor.utils import chunks +from trezor.ui.layouts import ( + confirm_metadata, + confirm_output, + confirm_path_warning, + confirm_properties, +) -from apps.common.confirm import confirm, require_confirm, require_hold_to_confirm from apps.common.layout import address_n_to_str from . import seed @@ -55,9 +55,6 @@ CERTIFICATE_TYPE_NAMES = { CardanoCertificateType.STAKE_POOL_REGISTRATION: "Stakepool registration", } -# Maximum number of characters per line in monospace font. -_MAX_MONO_LINE = 18 - def format_coin_amount(amount: int) -> str: return "%s %s" % (format_amount(amount, 6), "ADA") @@ -75,73 +72,72 @@ async def confirm_sending( ) -> None: await confirm_sending_token_bundle(ctx, token_bundle) - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Confirm sending:") - page1.bold(format_coin_amount(ada_amount)) - page1.normal("to") - - pages = _paginate_text( - page1, - "Confirm transaction", - ui.ICON_SEND, + await confirm_output( + ctx, to, - lines_per_page=4, - lines_used_on_first_page=3, + format_coin_amount(ada_amount), + title="Confirm transaction", + subtitle="Confirm sending:", + font_amount=ui.BOLD, + width_paginated=17, + to_str="\nto\n", + to_paginated=True, + br_code=ButtonRequestType.Other, ) - await require_confirm(ctx, Paginated(pages)) - async def confirm_sending_token_bundle( ctx: wire.Context, token_bundle: list[CardanoAssetGroupType] ) -> None: for token_group in token_bundle: for token in token_group.tokens: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Asset fingerprint:") - page1.bold( - format_asset_fingerprint( - policy_id=token_group.policy_id, - asset_name_bytes=token.asset_name_bytes, - ) + await confirm_properties( + ctx, + "confirm_token", + title="Confirm transaction", + props=[ + ( + "Asset fingerprint:", + format_asset_fingerprint( + policy_id=token_group.policy_id, + asset_name_bytes=token.asset_name_bytes, + ), + ), + ("Amount sent:", format_amount(token.amount, 0)), + ], + br_code=ButtonRequestType.Other, ) - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Amount sent:") - page2.bold(format_amount(token.amount, 0)) - await require_confirm(ctx, Paginated([page1, page2])) async def show_warning_tx_output_contains_tokens(ctx: wire.Context) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("The following") - page1.normal("transaction output") - page1.normal("contains tokens.") - page1.br_half() - page1.normal("Continue?") - - await require_confirm(ctx, page1) + await confirm_metadata( + ctx, + "confirm_tokens", + title="Confirm transaction", + content="The following\ntransaction output\ncontains tokens.", + larger_vspace=True, + br_code=ButtonRequestType.Other, + ) async def show_warning_path(ctx: wire.Context, path: list[int], title: str) -> None: - page1 = Text("Confirm path", ui.ICON_WRONG, ui.RED) - page1.normal(title) - page1.bold(address_n_to_str(path)) - page1.normal("is unknown.") - page1.normal("Are you sure?") - await require_confirm(ctx, page1) + await confirm_path_warning(ctx, address_n_to_str(path), path_type=title) async def show_warning_tx_no_staking_info( ctx: wire.Context, address_type: CardanoAddressType, amount: int ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Change " + ADDRESS_TYPE_NAMES[address_type].lower()) - page1.normal("address has no stake") - page1.normal("rights.") - page1.normal("Change amount:") - page1.bold(format_coin_amount(amount)) - - await require_confirm(ctx, page1) + atype = ADDRESS_TYPE_NAMES[address_type].lower() + content = "Change %s address has no stake rights.\nChange amount:\n{}" % atype + await confirm_metadata( + ctx, + "warning_staking", + title="Confirm transaction", + content=content, + param=format_coin_amount(amount), + hide_continue=True, + br_code=ButtonRequestType.Other, + ) async def show_warning_tx_pointer_address( @@ -149,21 +145,25 @@ async def show_warning_tx_pointer_address( pointer: CardanoBlockchainPointerType, amount: int, ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Change address has a") - page1.normal("pointer with staking") - page1.normal("rights.") - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Pointer:") - page2.bold( - "%s, %s, %s" - % (pointer.block_index, pointer.tx_index, pointer.certificate_index) + await confirm_properties( + ctx, + "warning_pointer", + title="Confirm transaction", + props=[ + ("Change address has a\npointer with staking\nrights.\n\n\n", None), + ( + "Pointer:", + "%s, %s, %s" + % ( + pointer.block_index, + pointer.tx_index, + pointer.certificate_index, + ), + ), + ("Change amount:", format_coin_amount(amount)), + ], + br_code=ButtonRequestType.Other, ) - page2.normal("Change amount:") - page2.bold(format_coin_amount(amount)) - - await require_confirm(ctx, Paginated([page1, page2])) async def show_warning_tx_different_staking_account( @@ -171,18 +171,23 @@ async def show_warning_tx_different_staking_account( staking_account_path: list[int], amount: int, ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Change address staking") - page1.normal("rights do not match") - page1.normal("the current account.") - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Staking account %s:" % format_account_number(staking_account_path)) - page2.bold(address_n_to_str(staking_account_path)) - page2.normal("Change amount:") - page2.bold(format_coin_amount(amount)) - - await require_confirm(ctx, Paginated([page1, page2])) + await confirm_properties( + ctx, + "warning_differentstaking", + title="Confirm transaction", + props=[ + ( + "Change address staking rights do not match the current account.\n\n", + None, + ), + ( + "Staking account %s:" % format_account_number(staking_account_path), + address_n_to_str(staking_account_path), + ), + ("Change amount:", format_coin_amount(amount)), + ], + br_code=ButtonRequestType.Other, + ) async def show_warning_tx_staking_key_hash( @@ -190,20 +195,19 @@ async def show_warning_tx_staking_key_hash( staking_key_hash: bytes, amount: int, ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Change address staking") - page1.normal("rights do not match") - page1.normal("the current account.") + props = [ + ("Change address staking rights do not match the current account.\n\n", None), + ("Staking key hash:", hexlify(staking_key_hash).decode()), + ("Change amount:", format_coin_amount(amount)), + ] - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Staking key hash:") - page2.mono(*chunks(hexlify(staking_key_hash).decode(), 17)) - - page3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page3.normal("Change amount:") - page3.bold(format_coin_amount(amount)) - - await require_confirm(ctx, Paginated([page1, page2, page3])) + await confirm_properties( + ctx, + "confirm_different_stakingrights", + title="Confirm transaction", + props=props, + br_code=ButtonRequestType.Other, + ) async def confirm_transaction( @@ -215,24 +219,27 @@ async def confirm_transaction( validity_interval_start: int | None, is_network_id_verifiable: bool, ) -> None: - pages: list[ui.Component] = [] + props = [ + ("Transaction amount:", format_coin_amount(amount)), + ("Transaction fee:", format_coin_amount(fee)), + ] - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Transaction amount:") - page1.bold(format_coin_amount(amount)) - page1.normal("Transaction fee:") - page1.bold(format_coin_amount(fee)) - pages.append(page1) - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) if is_network_id_verifiable: - page2.normal("Network:") - page2.bold(protocol_magics.to_ui_string(protocol_magic)) - page2.normal("Valid since: %s" % format_optional_int(validity_interval_start)) - page2.normal("TTL: %s" % format_optional_int(ttl)) - pages.append(page2) + props.append(("Network:", protocol_magics.to_ui_string(protocol_magic))) - await require_hold_to_confirm(ctx, Paginated(pages)) + props.append( + ("Valid since: %s" % format_optional_int(validity_interval_start), None) + ) + props.append(("TTL: %s" % format_optional_int(ttl), None)) + + await confirm_properties( + ctx, + "confirm_total", + title="Confirm transaction", + props=props, + hold=True, + br_code=ButtonRequestType.Other, + ) async def confirm_certificate( @@ -242,23 +249,24 @@ async def confirm_certificate( # in this call assert certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION - pages: list[ui.Component] = [] - - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Confirm:") - page1.bold(CERTIFICATE_TYPE_NAMES[certificate.type]) - page1.normal("for account %s:" % format_account_number(certificate.path)) - page1.bold(address_n_to_str(to_account_path(certificate.path))) - pages.append(page1) - + props = [ + ("Confirm:", CERTIFICATE_TYPE_NAMES[certificate.type]), + ( + "for account %s:" % format_account_number(certificate.path), + address_n_to_str(to_account_path(certificate.path)), + ), + ] if certificate.type == CardanoCertificateType.STAKE_DELEGATION: assert certificate.pool is not None # validate_certificate - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("to pool:") - page2.bold(format_stake_pool_id(certificate.pool)) - pages.append(page2) + props.append(("to pool:", format_stake_pool_id(certificate.pool))) - await require_confirm(ctx, Paginated(pages)) + await confirm_properties( + ctx, + "confirm_certificate", + title="Confirm transaction", + props=props, + br_code=ButtonRequestType.Other, + ) async def confirm_stake_pool_parameters( @@ -267,25 +275,31 @@ async def confirm_stake_pool_parameters( network_id: int, protocol_magic: int, ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.bold("Stake pool registration") - page1.normal("Pool ID:") - page1.bold(format_stake_pool_id(pool_parameters.pool_id)) - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Pool reward account:") - page2.bold(pool_parameters.reward_account) - - page3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page3.normal("Pledge: " + format_coin_amount(pool_parameters.pledge)) - page3.normal("Cost: " + format_coin_amount(pool_parameters.cost)) margin_percentage = ( 100.0 * pool_parameters.margin_numerator / pool_parameters.margin_denominator ) percentage_formatted = ("%f" % margin_percentage).rstrip("0").rstrip(".") - page3.normal("Margin: %s%%" % percentage_formatted) - - await require_confirm(ctx, Paginated([page1, page2, page3])) + await confirm_properties( + ctx, + "confirm_pool_registration", + title="Confirm transaction", + props=[ + ( + "Stake pool registration\nPool ID:", + format_stake_pool_id(pool_parameters.pool_id), + ), + ("Pool reward account:", pool_parameters.reward_account), + ( + "Pledge: {}\nCost: {}\nMargin: {}%".format( + format_coin_amount(pool_parameters.pledge), + format_coin_amount(pool_parameters.cost), + percentage_formatted, + ), + None, + ), + ], + br_code=ButtonRequestType.Other, + ) async def confirm_stake_pool_owners( @@ -294,56 +308,67 @@ async def confirm_stake_pool_owners( owners: list[CardanoPoolOwnerType], network_id: int, ) -> None: - pages: list[ui.Component] = [] + props: list[tuple[str, str | None]] = [] for index, owner in enumerate(owners, 1): - page = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page.normal("Pool owner #%d:" % (index)) - if owner.staking_key_path: - page.bold(address_n_to_str(owner.staking_key_path)) - page.normal( - encode_human_readable_address( - pack_reward_address_bytes( - get_public_key_hash(keychain, owner.staking_key_path), - network_id, - ) + props.append( + ("Pool owner #%d:" % index, address_n_to_str(owner.staking_key_path)) + ) + props.append( + ( + encode_human_readable_address( + pack_reward_address_bytes( + get_public_key_hash(keychain, owner.staking_key_path), + network_id, + ) + ), + None, ) ) else: assert owner.staking_key_hash is not None # validate_pool_owners - page.bold( - encode_human_readable_address( - pack_reward_address_bytes(owner.staking_key_hash, network_id) + props.append( + ( + "Pool owner #%d:" % index, + encode_human_readable_address( + pack_reward_address_bytes(owner.staking_key_hash, network_id) + ), ) ) - pages.append(page) - - await require_confirm(ctx, Paginated(pages)) + await confirm_properties( + ctx, + "confirm_pool_owners", + title="Confirm transaction", + props=props, + br_code=ButtonRequestType.Other, + ) async def confirm_stake_pool_metadata( ctx: wire.Context, metadata: CardanoPoolMetadataType | None, ) -> None: - if metadata is None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Pool has no metadata") - page1.normal("(anonymous pool)") - - await require_confirm(ctx, page1) + await confirm_properties( + ctx, + "confirm_pool_metadata", + title="Confirm transaction", + props=[("Pool has no metadata (anonymous pool)", None)], + br_code=ButtonRequestType.Other, + ) return - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Pool metadata url:") - page1.bold(metadata.url) - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Pool metadata hash:") - page2.bold(hexlify(metadata.hash).decode()) - - await require_confirm(ctx, Paginated([page1, page2])) + await confirm_properties( + ctx, + "confirm_pool_metadata", + title="Confirm transaction", + props=[ + ("Pool metadata url:", metadata.url), + ("Pool metadata hash:", hexlify(metadata.hash).decode()), + ], + br_code=ButtonRequestType.Other, + ) async def confirm_transaction_network_ttl( @@ -352,36 +377,53 @@ async def confirm_transaction_network_ttl( ttl: int | None, validity_interval_start: int | None, ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Network:") - page1.bold(protocol_magics.to_ui_string(protocol_magic)) - page1.normal("Valid since: %s" % format_optional_int(validity_interval_start)) - page1.normal("TTL: %s" % format_optional_int(ttl)) - - await require_confirm(ctx, page1) + await confirm_properties( + ctx, + "confirm_pool_network", + title="Confirm transaction", + props=[ + ("Network:", protocol_magics.to_ui_string(protocol_magic)), + ( + "Valid since: %s" % format_optional_int(validity_interval_start), + None, + ), + ("TTL: %s" % format_optional_int(ttl), None), + ], + br_code=ButtonRequestType.Other, + ) async def confirm_stake_pool_registration_final( ctx: wire.Context, ) -> None: - - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Confirm signing the stake pool registration as an owner") - - await require_hold_to_confirm(ctx, page1) + await confirm_metadata( + ctx, + "confirm_pool_final", + title="Confirm transaction", + content="Confirm signing the stake pool registration as an owner", + hide_continue=True, + hold=True, + br_code=ButtonRequestType.Other, + ) async def confirm_withdrawal( ctx: wire.Context, withdrawal: CardanoTxWithdrawalType ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Confirm withdrawal") - page1.normal("for account %s:" % format_account_number(withdrawal.path)) - page1.bold(address_n_to_str(to_account_path(withdrawal.path))) - page1.normal("Amount:") - page1.bold(format_coin_amount(withdrawal.amount)) - - await require_confirm(ctx, page1) + await confirm_properties( + ctx, + "confirm_withdrawal", + title="Confirm transaction", + props=[ + ( + "Confirm withdrawal\nfor account %s:" + % format_account_number(withdrawal.path), + address_n_to_str(to_account_path(withdrawal.path)), + ), + ("Amount:", format_coin_amount(withdrawal.amount)), + ], + br_code=ButtonRequestType.Other, + ) async def confirm_catalyst_registration( @@ -391,166 +433,98 @@ async def confirm_catalyst_registration( reward_address: str, nonce: int, ) -> None: - pages: list[ui.Component] = [] - - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.bold("Catalyst voting key") - page1.bold("registration") - pages.append(page1) - - page2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page2.normal("Voting public key:") - page2.bold(*chunks(public_key, 17)) - pages.append(page2) - - page3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page3.normal("Staking key for") - page3.normal("account %s:" % format_account_number(staking_path)) - page3.bold(address_n_to_str(staking_path)) - pages.append(page3) - - page4 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page4.normal("Rewards go to:") - - pages.extend( - _paginate_text(page4, "Confirm transaction", ui.ICON_SEND, reward_address) + await confirm_properties( + ctx, + "confirm_catalyst_registration", + title="Confirm transaction", + props=[ + ("Catalyst voting key registration", None), + ("Voting public key:", public_key), + ( + "Staking key for account %s:" % format_account_number(staking_path), + address_n_to_str(staking_path), + ), + ("Rewards go to:", reward_address), + ("Nonce:", str(nonce)), + ], + br_code=ButtonRequestType.Other, ) - last_page = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - last_page.normal("Nonce: %s" % nonce) - pages.append(last_page) - - await require_confirm(ctx, Paginated(pages)) - async def show_auxiliary_data_hash( ctx: wire.Context, auxiliary_data_hash: bytes ) -> None: - page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) - page1.normal("Auxiliary data hash:") - page1.bold(hexlify(auxiliary_data_hash).decode()) - - await require_confirm(ctx, page1) - - -async def show_address( - ctx: wire.Context, - address: str, - address_type: CardanoAddressType, - path: list[int], - network: str | None = None, -) -> bool: - """ - Custom show_address function is needed because cardano addresses don't - fit on a single screen. - """ - - address_type_label = "%s address" % ADDRESS_TYPE_NAMES[address_type] - page1 = Text(address_type_label, ui.ICON_RECEIVE, ui.GREEN) - - lines_per_page = 5 - lines_used_on_first_page = 0 - - # assemble first page to be displayed (path + network + whatever part of the address fits) - if network is not None: - page1.normal("%s network" % network) - lines_used_on_first_page += 1 - - path_str = address_n_to_str(path) - page1.mono(path_str) - lines_used_on_first_page = min( - lines_used_on_first_page + math.ceil(len(path_str) / _MAX_MONO_LINE), - lines_per_page, - ) - - pages = _paginate_text( - page1, - address_type_label, - ui.ICON_RECEIVE, - address, - lines_per_page=lines_per_page, - lines_used_on_first_page=lines_used_on_first_page, - ) - - return await confirm( + await confirm_properties( ctx, - Paginated(pages), - code=ButtonRequestType.Address, - cancel="QR", - cancel_style=ButtonDefault, + "confirm_auxiliary_data", + title="Confirm transaction", + props=[("Auxiliary data hash:", hexlify(auxiliary_data_hash).decode())], + br_code=ButtonRequestType.Other, ) -def _paginate_text( - first_page: Text, - page_desc: str, - page_icon: str, - text: str, - lines_per_page: int = 5, - lines_used_on_first_page: int = 1, -) -> list[ui.Component]: - lines = list(chunks(text, 17)) - - offset = lines_per_page - lines_used_on_first_page - - for text_line in lines[:offset]: - first_page.bold(text_line) - - pages: list[ui.Component] = [first_page] - if len(lines) > offset: - to_pages = list(chunks(lines[offset:], lines_per_page)) - for page in to_pages: - t = Text(page_desc, page_icon, ui.GREEN) - for line in page: - t.bold(line) - pages.append(t) - - return pages - - async def show_warning_address_foreign_staking_key( ctx: wire.Context, account_path: list[int], staking_account_path: list[int], staking_key_hash: bytes | None, ) -> None: - page1 = Text("Warning", ui.ICON_WRONG, ui.RED) - page1.normal("Stake rights associated") - page1.normal("with this address do") - page1.normal("not match your") - page1.normal("account %s:" % format_account_number(account_path)) - page1.bold(address_n_to_str(account_path)) + props: list[tuple[str, str | None]] = [ + ( + "Stake rights associated with this address do not match your account %s:" + % format_account_number(account_path), + address_n_to_str(account_path), + ) + ] - page2 = Text("Warning", ui.ICON_WRONG, ui.RED) if staking_account_path: - page2.normal("Stake account %s:" % format_account_number(staking_account_path)) - page2.bold(address_n_to_str(staking_account_path)) - page2.br_half() + props.append( + ( + "Stake account %s:" % format_account_number(staking_account_path), + address_n_to_str(staking_account_path), + ) + ) else: assert staking_key_hash is not None # _validate_base_address_staking_info - page2.normal("Staking key:") - page2.bold(hexlify(staking_key_hash).decode()) - page2.normal("Continue?") + props.append(("Staking key:", hexlify(staking_key_hash).decode())) + props.append(("Continue?", None)) - await require_confirm(ctx, Paginated([page1, page2])) + await confirm_properties( + ctx, + "warning_foreign_stakingkey", + title="Warning", + props=props, + icon=ui.ICON_WRONG, + icon_color=ui.RED, + br_code=ButtonRequestType.Other, + ) async def show_warning_tx_network_unverifiable(ctx: wire.Context) -> None: - page1 = Text("Warning", ui.ICON_SEND, ui.GREEN) - page1.normal("Transaction has no outputs, network cannot be verified.") - page1.br_half() - page1.normal("Continue?") - - await require_confirm(ctx, page1) + await confirm_metadata( + ctx, + "warning_no_outputs", + title="Warning", + content="Transaction has no outputs, network cannot be verified.", + larger_vspace=True, + br_code=ButtonRequestType.Other, + ) async def show_warning_address_pointer( ctx: wire.Context, pointer: CardanoBlockchainPointerType ) -> None: - text = Text("Warning", ui.ICON_WRONG, ui.RED) - text.normal("Pointer address:") - text.normal("Block: %s" % pointer.block_index) - text.normal("Transaction: %s" % pointer.tx_index) - text.normal("Certificate: %s" % pointer.certificate_index) - text.normal("Continue?") - await require_confirm(ctx, text) + content = "Pointer address:\nBlock: %s\nTransaction: %s\nCertificate: %s" % ( + pointer.block_index, + pointer.tx_index, + pointer.certificate_index, + ) + await confirm_metadata( + ctx, + "warning_pointer", + title="Warning", + icon=ui.ICON_WRONG, + icon_color=ui.RED, + content=content, + br_code=ButtonRequestType.Other, + ) diff --git a/core/src/apps/common/layout.py b/core/src/apps/common/layout.py index 2dda573e68..17f546bb70 100644 --- a/core/src/apps/common/layout.py +++ b/core/src/apps/common/layout.py @@ -1,42 +1,9 @@ -from micropython import const - -from trezor import ui -from trezor.enums import ButtonRequestType -from trezor.ui.components.tt.button import ButtonDefault -from trezor.ui.components.tt.text import Text -from trezor.ui.container import Container -from trezor.ui.qr import Qr from trezor.utils import chunks from apps.common import HARDENED -from apps.common.confirm import confirm if False: from typing import Iterable, Iterator - from trezor import wire - - -async def show_qr( - ctx: wire.Context, - address: str, - desc: str = "Confirm address", - cancel: str = "Address", -) -> bool: - QR_X = const(120) - QR_Y = const(115) - QR_SIZE_THRESHOLD = const(63) - QR_COEF = const(4) if len(address) < QR_SIZE_THRESHOLD else const(3) - qr = Qr(address, QR_X, QR_Y, QR_COEF) - text = Text(desc, ui.ICON_RECEIVE, ui.GREEN) - content = Container(qr, text) - - return await confirm( - ctx, - content, - code=ButtonRequestType.Address, - cancel=cancel, - cancel_style=ButtonDefault, - ) def split_address(address: str) -> Iterator[str]: diff --git a/core/src/trezor/ui/components/tt/scroll.py b/core/src/trezor/ui/components/tt/scroll.py index b25999097a..c0c9451c0b 100644 --- a/core/src/trezor/ui/components/tt/scroll.py +++ b/core/src/trezor/ui/components/tt/scroll.py @@ -10,7 +10,7 @@ from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe from .text import TEXT_MAX_LINES, Span, Text if False: - from typing import Iterable, Any + from typing import Callable, Iterable from ..common.text import TextContent @@ -266,7 +266,8 @@ def paginate_text( header_icon: str = ui.ICON_DEFAULT, icon_color: int = ui.ORANGE_ICON, break_words: bool = False, -) -> Confirm | Paginated: + confirm: Callable[[ui.Component], ui.Layout] = Confirm, +) -> ui.Layout: span = Span(text, 0, font, break_words=break_words) if span.count_lines() <= TEXT_MAX_LINES: result = Text( @@ -277,7 +278,7 @@ def paginate_text( break_words=break_words, ) result.content = [font, text] - return Confirm(result) + return confirm(result) else: pages: list[ui.Component] = [] @@ -305,7 +306,7 @@ def paginate_text( for _ in range(TEXT_MAX_LINES - 1): span.next_line() - pages[-1] = Confirm(pages[-1]) + pages[-1] = confirm(pages[-1]) return Paginated(pages) @@ -315,8 +316,8 @@ def paginate_paragraphs( header_icon: str = ui.ICON_DEFAULT, icon_color: int = ui.ORANGE_ICON, break_words: bool = False, - confirm_kwargs: Dict[str, Any] = {}, -) -> Union[Confirm, Paginated]: + confirm: Callable[[ui.Component], ui.Layout] = Confirm, +) -> ui.Layout: span = Span("", 0, ui.NORMAL, break_words=break_words) lines = 0 content: list[TextContent] = [] @@ -343,7 +344,7 @@ def paginate_paragraphs( result.content.append("\n") result.content.append(font) result.content.append(text) - return Confirm(result, **confirm_kwargs) + return confirm(result) else: pages: list[ui.Component] = [] @@ -373,5 +374,5 @@ def paginate_paragraphs( else: lines_left -= 1 - pages[-1] = Confirm(pages[-1], **confirm_kwargs) + pages[-1] = confirm(pages[-1]) return Paginated(pages) diff --git a/core/src/trezor/ui/layouts/tt.py b/core/src/trezor/ui/layouts/tt.py index 09be0dc1c4..246408e4b6 100644 --- a/core/src/trezor/ui/layouts/tt.py +++ b/core/src/trezor/ui/layouts/tt.py @@ -25,15 +25,18 @@ from .common import interact if False: from typing import ( + Awaitable, Iterator, + NoReturn, + Optional, Sequence, + Tuple, Type, Union, - Awaitable, - NoReturn, ) ExceptionType = Union[BaseException, Type[BaseException]] + PropertyType = Tuple[str, Optional[str]] __all__ = ( @@ -52,6 +55,7 @@ __all__ = ( "confirm_output", "confirm_decred_sstx_submission", "confirm_hex", + "confirm_properties", "confirm_total", "confirm_total_ethereum", "confirm_total_ripple", @@ -182,9 +186,11 @@ async def confirm_backup(ctx: wire.GenericContext) -> bool: return confirmed -async def confirm_path_warning(ctx: wire.GenericContext, path: str) -> None: +async def confirm_path_warning( + ctx: wire.GenericContext, path: str, path_type: str = "Path" +) -> None: text = Text("Confirm path", ui.ICON_WRONG, ui.RED) - text.normal("Path") + text.normal(path_type) text.mono(*break_path_to_lines(path, MONO_ADDR_PER_LINE)) text.normal("is unknown.", "Are you sure?") await raise_if_cancelled( @@ -237,8 +243,11 @@ def _show_address( address: str, title: str, network: str | None = None, -) -> Confirm | Paginated: + extra: str | None = None, +) -> ui.Layout: para = [(ui.NORMAL, "%s network" % network)] if network is not None else [] + if extra is not None: + para.append((ui.BOLD, extra)) para.extend( (ui.MONO, address_line) for address_line in chunks(address, MONO_ADDR_PER_LINE) ) @@ -247,7 +256,9 @@ def _show_address( header=title, header_icon=ui.ICON_RECEIVE, icon_color=ui.GREEN, - confirm_kwargs={"cancel": "QR", "cancel_style": ButtonDefault}, + confirm=lambda content: Confirm( + content, cancel="QR", cancel_style=ButtonDefault + ), ) @@ -290,6 +301,8 @@ async def show_address( network: str | None = None, multisig_index: int | None = None, xpubs: Sequence[str] = [], + address_extra: str | None = None, + title_qr: str | None = None, ) -> None: is_multisig = len(xpubs) > 0 while True: @@ -300,6 +313,7 @@ async def show_address( address, title, network, + extra=address_extra, ), "show_address", ButtonRequestType.Address, @@ -311,7 +325,7 @@ async def show_address( ctx, _show_qr( address if address_qr is None else address_qr, - title, + title if title_qr is None else title_qr, cancel="XPUBs" if is_multisig else "Address", ), "show_qr", @@ -455,19 +469,29 @@ async def confirm_output( address: str, amount: str, font_amount: int = ui.NORMAL, # TODO cleanup @ redesign + title: str = "Confirm sending", + subtitle: str | None = None, # TODO cleanup @ redesign color_to: int = ui.FG, # TODO cleanup @ redesign to_str: str = " to\n", # TODO cleanup @ redesign + to_paginated: bool = False, # TODO cleanup @ redesign width: int = MONO_ADDR_PER_LINE, width_paginated: int = MONO_ADDR_PER_LINE - 1, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, ) -> None: - title = "Confirm sending" - if len(address) > (TEXT_MAX_LINES - 1) * width: - para = [(font_amount, amount)] + header_lines = to_str.count("\n") + int(subtitle is not None) + if len(address) > (TEXT_MAX_LINES - header_lines) * width: + para = [] + if subtitle is not None: + para.append((ui.NORMAL, subtitle)) + para.append((font_amount, amount)) + if to_paginated: + para.append((ui.NORMAL, "to")) para.extend((ui.MONO, line) for line in chunks(address, width_paginated)) content: ui.Layout = paginate_paragraphs(para, title, ui.ICON_SEND, ui.GREEN) else: text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False) + if subtitle is not None: + text.normal(subtitle, "\n") text.content = [font_amount, amount, ui.NORMAL, color_to, to_str, ui.FG] text.mono(*chunks_intersperse(address, width)) content = Confirm(text) @@ -552,6 +576,28 @@ async def confirm_hex( await raise_if_cancelled(interact(ctx, content, br_type, br_code)) +# TODO keep name and value on the same page if possible +async def confirm_properties( + ctx: wire.GenericContext, + br_type: str, + title: str, + props: Sequence[PropertyType], + icon: str = ui.ICON_SEND, # TODO cleanup @ redesign + icon_color: int = ui.GREEN, # TODO cleanup @ redesign + hold: bool = False, + br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, +) -> None: + para = [] + for p in props: + para.append((ui.NORMAL, p[0])) + if p[1] is not None: + para.append((ui.BOLD, p[1])) + content = paginate_paragraphs( + para, title, icon, icon_color, confirm=HoldToConfirm if hold else Confirm + ) + await raise_if_cancelled(interact(ctx, content, br_type, br_code)) + + async def confirm_total( ctx: wire.GenericContext, total_amount: str, @@ -630,6 +676,7 @@ async def confirm_metadata( param_font: int = ui.BOLD, icon: str = ui.ICON_SEND, # TODO cleanup @ redesign icon_color: int = ui.GREEN, # TODO cleanup @ redesign + larger_vspace: bool = False, # TODO cleanup @ redesign ) -> None: text = Text(title, icon, icon_color, new_lines=False) text.format_parametrized( @@ -638,6 +685,8 @@ async def confirm_metadata( if not hide_continue: text.br() + if larger_vspace: + text.br_half() text.normal("Continue?") cls = HoldToConfirm if hold else Confirm diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index 47dd39d56b..ba78d90d3c 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -107,7 +107,6 @@ def test_cardano_sign_tx_with_multiple_chunks(client, parameters, result): # If that changes, we'll need to figure out something else. messages.ButtonRequest(page_number=1), messages.ButtonRequest(page_number=2), - messages.ButtonRequest(page_number=3), messages.ButtonRequest(page_number=1), messages.ButtonRequest(page_number=2), ]