from typing import TYPE_CHECKING from trezor import ui from trezor.enums import ( ButtonRequestType, CardanoAddressType, CardanoCertificateType, CardanoNativeScriptType, ) from trezor.strings import format_amount from trezor.ui import layouts from apps.common.paths import address_n_to_str from . import addresses from .helpers import bech32, protocol_magics from .helpers.utils import ( format_account_number, format_asset_fingerprint, format_optional_int, format_stake_pool_id, ) confirm_metadata = layouts.confirm_metadata # global_import_cache confirm_properties = layouts.confirm_properties # global_import_cache if TYPE_CHECKING: from typing import Literal from trezor.wire import Context from trezor import messages from trezor.enums import CardanoNativeScriptHashDisplayFormat from trezor.ui.layouts import PropertyType from .helpers.credential import Credential from .seed import Keychain ADDRESS_TYPE_NAMES = { CardanoAddressType.BYRON: "Legacy", CardanoAddressType.BASE: "Base", CardanoAddressType.BASE_SCRIPT_KEY: "Base", CardanoAddressType.BASE_KEY_SCRIPT: "Base", CardanoAddressType.BASE_SCRIPT_SCRIPT: "Base", CardanoAddressType.POINTER: "Pointer", CardanoAddressType.POINTER_SCRIPT: "Pointer", CardanoAddressType.ENTERPRISE: "Enterprise", CardanoAddressType.ENTERPRISE_SCRIPT: "Enterprise", CardanoAddressType.REWARD: "Reward", CardanoAddressType.REWARD_SCRIPT: "Reward", } SCRIPT_TYPE_NAMES = { CardanoNativeScriptType.PUB_KEY: "Key", CardanoNativeScriptType.ALL: "All", CardanoNativeScriptType.ANY: "Any", CardanoNativeScriptType.N_OF_K: "N of K", CardanoNativeScriptType.INVALID_BEFORE: "Invalid before", CardanoNativeScriptType.INVALID_HEREAFTER: "Invalid hereafter", } CERTIFICATE_TYPE_NAMES = { CardanoCertificateType.STAKE_REGISTRATION: "Stake key registration", CardanoCertificateType.STAKE_DEREGISTRATION: "Stake key deregistration", CardanoCertificateType.STAKE_DELEGATION: "Stake delegation", CardanoCertificateType.STAKE_POOL_REGISTRATION: "Stakepool registration", } BRT_Other = ButtonRequestType.Other # global_import_cache CVOTE_REWARD_ELIGIBILITY_WARNING = ( "Warning: The address is not a payment address, it is not eligible for rewards." ) def format_coin_amount(amount: int, network_id: int) -> str: from .helpers import network_ids currency = "ADA" if network_ids.is_mainnet(network_id) else "tADA" return f"{format_amount(amount, 6)} {currency}" async def show_native_script( ctx: Context, script: messages.CardanoNativeScript, indices: list[int] | None = None, ) -> None: CNST = CardanoNativeScriptType # local_cache_global script_type = script.type # local_cache_attribute key_path = script.key_path # local_cache_attribute key_hash = script.key_hash # local_cache_attribute scripts = script.scripts # local_cache_attribute script_heading = "Script" if indices is None: indices = [] if indices: script_heading += " " + ".".join(str(i) for i in indices) script_type_name_suffix = "" if script_type == CNST.PUB_KEY: if key_path: script_type_name_suffix = "path" elif key_hash: script_type_name_suffix = "hash" props: list[PropertyType] = [ ( f"{script_heading} - {SCRIPT_TYPE_NAMES[script_type]} {script_type_name_suffix}:", None, ) ] append = props.append # local_cache_attribute if script_type == CNST.PUB_KEY: assert key_hash is not None or key_path # validate_script if key_hash: append((None, bech32.encode(bech32.HRP_SHARED_KEY_HASH, key_hash))) elif key_path: append((address_n_to_str(key_path), None)) elif script_type == CNST.N_OF_K: assert script.required_signatures_count is not None # validate_script append( ( f"Requires {script.required_signatures_count} out of {len(scripts)} signatures.", None, ) ) elif script_type == CNST.INVALID_BEFORE: assert script.invalid_before is not None # validate_script append((str(script.invalid_before), None)) elif script_type == CNST.INVALID_HEREAFTER: assert script.invalid_hereafter is not None # validate_script append((str(script.invalid_hereafter), None)) if script_type in ( CNST.ALL, CNST.ANY, CNST.N_OF_K, ): assert scripts # validate_script append((f"Contains {len(scripts)} nested scripts.", None)) await confirm_properties( ctx, "verify_script", "Verify script", props, br_code=BRT_Other, ) for i, sub_script in enumerate(scripts): await show_native_script(ctx, sub_script, indices + [i + 1]) async def show_script_hash( ctx: Context, script_hash: bytes, display_format: CardanoNativeScriptHashDisplayFormat, ) -> None: from trezor.enums import CardanoNativeScriptHashDisplayFormat assert display_format in ( CardanoNativeScriptHashDisplayFormat.BECH32, CardanoNativeScriptHashDisplayFormat.POLICY_ID, ) if display_format == CardanoNativeScriptHashDisplayFormat.BECH32: await confirm_properties( ctx, "verify_script", "Verify script", (("Script hash:", bech32.encode(bech32.HRP_SCRIPT_HASH, script_hash)),), br_code=BRT_Other, ) elif display_format == CardanoNativeScriptHashDisplayFormat.POLICY_ID: await layouts.confirm_blob( ctx, "verify_script", "Verify script", script_hash, "Policy ID:", br_code=BRT_Other, ) async def show_tx_init(ctx: Context, title: str) -> bool: should_show_details = await layouts.should_show_more( ctx, "Confirm transaction", ( ( ui.BOLD, title, ), (ui.NORMAL, "Choose level of details:"), ), "Show All", confirm="Show Simple", ) return should_show_details async def confirm_input(ctx: Context, input: messages.CardanoTxInput) -> None: await confirm_properties( ctx, "confirm_input", "Confirm transaction", ( ("Input ID:", input.prev_hash), ("Input index:", str(input.prev_index)), ), br_code=BRT_Other, ) async def confirm_sending( ctx: Context, ada_amount: int, to: str, output_type: Literal["address", "change", "collateral-return"], network_id: int, ) -> None: if output_type == "address": title = "Sending" elif output_type == "change": title = "Change output" elif output_type == "collateral-return": title = "Collateral return" else: raise RuntimeError # should be unreachable await layouts.confirm_output( ctx, to, format_coin_amount(ada_amount, network_id), title, br_code=ButtonRequestType.Other, ) async def confirm_sending_token( ctx: Context, policy_id: bytes, token: messages.CardanoToken ) -> None: assert token.amount is not None # _validate_token await confirm_properties( ctx, "confirm_token", "Confirm transaction", ( ( "Asset fingerprint:", format_asset_fingerprint( policy_id=policy_id, asset_name_bytes=token.asset_name_bytes, ), ), ("Amount sent:", format_amount(token.amount, 0)), ), br_code=BRT_Other, ) async def confirm_datum_hash(ctx: Context, datum_hash: bytes) -> None: await confirm_properties( ctx, "confirm_datum_hash", "Confirm transaction", ( ( "Datum hash:", bech32.encode(bech32.HRP_OUTPUT_DATUM_HASH, datum_hash), ), ), br_code=BRT_Other, ) async def confirm_inline_datum( ctx: Context, first_chunk: bytes, inline_datum_size: int ) -> None: await _confirm_data_chunk( ctx, "confirm_inline_datum", "Inline datum", first_chunk, inline_datum_size, ) async def confirm_reference_script( ctx: Context, first_chunk: bytes, reference_script_size: int ) -> None: await _confirm_data_chunk( ctx, "confirm_reference_script", "Reference script", first_chunk, reference_script_size, ) async def _confirm_data_chunk( ctx: Context, br_type: str, title: str, first_chunk: bytes, data_size: int ) -> None: MAX_DISPLAYED_SIZE = 56 displayed_bytes = first_chunk[:MAX_DISPLAYED_SIZE] bytes_optional_plural = "byte" if data_size == 1 else "bytes" props: list[tuple[str, bytes | None]] = [ ( f"{title} ({data_size} {bytes_optional_plural}):", displayed_bytes, ) ] if data_size > MAX_DISPLAYED_SIZE: props.append(("...", None)) await confirm_properties( ctx, br_type, title="Confirm transaction", props=props, br_code=BRT_Other, ) async def show_credentials( ctx: Context, payment_credential: Credential, stake_credential: Credential, ) -> None: intro_text = "Address" await _show_credential(ctx, payment_credential, intro_text, purpose="address") await _show_credential(ctx, stake_credential, intro_text, purpose="address") async def show_change_output_credentials( ctx: Context, payment_credential: Credential, stake_credential: Credential, ) -> None: intro_text = "The following address is a change address. Its" await _show_credential(ctx, payment_credential, intro_text, purpose="output") await _show_credential(ctx, stake_credential, intro_text, purpose="output") async def show_device_owned_output_credentials( ctx: Context, payment_credential: Credential, stake_credential: Credential, show_both_credentials: bool, ) -> None: intro_text = "The following address is owned by this device. Its" await _show_credential(ctx, payment_credential, intro_text, purpose="output") if show_both_credentials: await _show_credential(ctx, stake_credential, intro_text, purpose="output") async def show_cvote_registration_payment_credentials( ctx: Context, payment_credential: Credential, stake_credential: Credential, show_both_credentials: bool, show_payment_warning: bool, ) -> None: intro_text = ( "The vote key registration payment address is owned by this device. Its" ) await _show_credential( ctx, payment_credential, intro_text, purpose="cvote_reg_payment_address" ) if show_both_credentials or show_payment_warning: extra_text = CVOTE_REWARD_ELIGIBILITY_WARNING if show_payment_warning else None await _show_credential( ctx, stake_credential, intro_text, purpose="cvote_reg_payment_address", extra_text=extra_text, ) async def _show_credential( ctx: Context, credential: Credential, intro_text: str, purpose: Literal["address", "output", "cvote_reg_payment_address"], extra_text: str | None = None, ) -> None: title = { "address": f"{ADDRESS_TYPE_NAMES[credential.address_type]} address", "output": "Confirm transaction", "cvote_reg_payment_address": "Confirm transaction", }[purpose] props: list[PropertyType] = [] append = props.append # local_cache_attribute # Credential can be empty in case of enterprise address stake credential # and reward address payment credential. In that case we don't want to # show some of the "props". if credential.is_set(): credential_title = credential.get_title() append( ( f"{intro_text} {credential.type_name} credential is a {credential_title}:", None, ) ) props.extend(credential.format()) if credential.is_unusual_path: append((None, "Path is unusual.")) if credential.is_mismatch: append((None, "Credential doesn't match payment credential.")) if credential.is_reward and purpose != "cvote_reg_payment_address": # for cvote registrations, this is handled by extra_text at the end append(("Address is a reward address.", None)) if credential.is_no_staking: append( ( f"{ADDRESS_TYPE_NAMES[credential.address_type]} address - no staking rewards.", None, ) ) if extra_text: append((extra_text, None)) if len(props) > 0: await confirm_properties( ctx, "confirm_credential", title, props, br_code=BRT_Other, ) async def warn_path(ctx: Context, path: list[int], title: str) -> None: await layouts.confirm_path_warning(ctx, address_n_to_str(path), path_type=title) async def warn_tx_output_contains_tokens( ctx: Context, is_collateral_return: bool = False ) -> None: content = ( "The collateral return output contains tokens." if is_collateral_return else "The following transaction output contains tokens." ) await confirm_metadata( ctx, "confirm_tokens", "Confirm transaction", content, br_code=BRT_Other, ) async def warn_tx_contains_mint(ctx: Context) -> None: await confirm_metadata( ctx, "confirm_tokens", "Confirm transaction", "The transaction contains minting or burning of tokens.", br_code=BRT_Other, ) async def warn_tx_output_no_datum(ctx: Context) -> None: await confirm_metadata( ctx, "confirm_no_datum_hash", "Confirm transaction", "The following transaction output contains a script address, but does not contain a datum.", br_code=BRT_Other, ) async def warn_no_script_data_hash(ctx: Context) -> None: await confirm_metadata( ctx, "confirm_no_script_data_hash", "Confirm transaction", "The transaction contains no script data hash. Plutus script will not be able to run.", br_code=BRT_Other, ) async def warn_no_collateral_inputs(ctx: Context) -> None: await confirm_metadata( ctx, "confirm_no_collateral_inputs", "Confirm transaction", "The transaction contains no collateral inputs. Plutus script will not be able to run.", br_code=BRT_Other, ) async def warn_unknown_total_collateral(ctx: Context) -> None: await confirm_metadata( ctx, "confirm_unknown_total_collateral", "Warning", "Unknown collateral amount, check all items carefully.", br_code=BRT_Other, ) async def confirm_witness_request( ctx: Context, witness_path: list[int], ) -> None: from . import seed if seed.is_multisig_path(witness_path): path_title = "multi-sig path" elif seed.is_minting_path(witness_path): path_title = "token minting path" else: path_title = "path" await layouts.confirm_text( ctx, "confirm_total", "Confirm transaction", address_n_to_str(witness_path), f"Sign transaction with {path_title}:", BRT_Other, ) async def confirm_tx( ctx: Context, fee: int, network_id: int, protocol_magic: int, ttl: int | None, validity_interval_start: int | None, total_collateral: int | None, is_network_id_verifiable: bool, tx_hash: bytes | None, ) -> None: props: list[PropertyType] = [ ("Transaction fee:", format_coin_amount(fee, network_id)), ] append = props.append # local_cache_attribute if total_collateral is not None: append(("Total collateral:", format_coin_amount(total_collateral, network_id))) if is_network_id_verifiable: append((f"Network: {protocol_magics.to_ui_string(protocol_magic)}", None)) append((f"Valid since: {format_optional_int(validity_interval_start)}", None)) append((f"TTL: {format_optional_int(ttl)}", None)) if tx_hash: append(("Transaction ID:", tx_hash)) await confirm_properties( ctx, "confirm_total", "Confirm transaction", props, hold=True, br_code=BRT_Other, ) async def confirm_certificate( ctx: Context, certificate: messages.CardanoTxCertificate ) -> None: # stake pool registration requires custom confirmation logic not covered # in this call assert certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION props: list[PropertyType] = [ ("Confirm:", CERTIFICATE_TYPE_NAMES[certificate.type]), _format_stake_credential( certificate.path, certificate.script_hash, certificate.key_hash ), ] if certificate.type == CardanoCertificateType.STAKE_DELEGATION: assert certificate.pool is not None # validate_certificate props.append(("to pool:", format_stake_pool_id(certificate.pool))) await confirm_properties( ctx, "confirm_certificate", "Confirm transaction", props, br_code=BRT_Other, ) async def confirm_stake_pool_parameters( ctx: Context, pool_parameters: messages.CardanoPoolParametersType, network_id: int, ) -> None: margin_percentage = ( 100.0 * pool_parameters.margin_numerator / pool_parameters.margin_denominator ) percentage_formatted = str(float(margin_percentage)).rstrip("0").rstrip(".") await confirm_properties( ctx, "confirm_pool_registration", "Confirm transaction", ( ( "Stake pool registration\nPool ID:", format_stake_pool_id(pool_parameters.pool_id), ), ("Pool reward account:", pool_parameters.reward_account), ( f"Pledge: {format_coin_amount(pool_parameters.pledge, network_id)}\n" + f"Cost: {format_coin_amount(pool_parameters.cost, network_id)}\n" + f"Margin: {percentage_formatted}%", None, ), ), br_code=BRT_Other, ) async def confirm_stake_pool_owner( ctx: Context, keychain: Keychain, owner: messages.CardanoPoolOwner, protocol_magic: int, network_id: int, ) -> None: from trezor import messages props: list[tuple[str, str | None]] = [] if owner.staking_key_path: props.append(("Pool owner:", address_n_to_str(owner.staking_key_path))) props.append( ( addresses.derive_human_readable( keychain, messages.CardanoAddressParametersType( address_type=CardanoAddressType.REWARD, address_n=owner.staking_key_path, ), protocol_magic, network_id, ), None, ) ) else: assert owner.staking_key_hash is not None # validate_pool_owners props.append( ( "Pool owner:", addresses.derive_human_readable( keychain, messages.CardanoAddressParametersType( address_type=CardanoAddressType.REWARD, staking_key_hash=owner.staking_key_hash, ), protocol_magic, network_id, ), ) ) await confirm_properties( ctx, "confirm_pool_owners", "Confirm transaction", props, br_code=BRT_Other, ) async def confirm_stake_pool_metadata( ctx: Context, metadata: messages.CardanoPoolMetadataType | None, ) -> None: if metadata is None: await confirm_properties( ctx, "confirm_pool_metadata", "Confirm transaction", (("Pool has no metadata (anonymous pool)", None),), br_code=BRT_Other, ) return await confirm_properties( ctx, "confirm_pool_metadata", "Confirm transaction", ( ("Pool metadata url:", metadata.url), ("Pool metadata hash:", metadata.hash), ), br_code=BRT_Other, ) async def confirm_stake_pool_registration_final( ctx: Context, protocol_magic: int, ttl: int | None, validity_interval_start: int | None, ) -> None: await confirm_properties( ctx, "confirm_pool_final", "Confirm transaction", ( ("Confirm signing the stake pool registration as an owner.", None), ("Network:", protocol_magics.to_ui_string(protocol_magic)), ("Valid since:", format_optional_int(validity_interval_start)), ("TTL:", format_optional_int(ttl)), ), hold=True, br_code=BRT_Other, ) async def confirm_withdrawal( ctx: Context, withdrawal: messages.CardanoTxWithdrawal, address_bytes: bytes, network_id: int, ) -> None: address_type_name = "script reward" if withdrawal.script_hash else "reward" address = addresses.encode_human_readable(address_bytes) props: list[PropertyType] = [ (f"Confirm withdrawal for {address_type_name} address:", address), ] if withdrawal.path: props.append( _format_stake_credential( withdrawal.path, withdrawal.script_hash, withdrawal.key_hash ) ) props.append(("Amount:", format_coin_amount(withdrawal.amount, network_id))) await confirm_properties( ctx, "confirm_withdrawal", "Confirm transaction", props, br_code=BRT_Other, ) def _format_stake_credential( path: list[int], script_hash: bytes | None, key_hash: bytes | None ) -> tuple[str, str]: from .helpers.utils import to_account_path if path: return ( f"for account {format_account_number(path)}:", address_n_to_str(to_account_path(path)), ) elif key_hash: return ("for key hash:", bech32.encode(bech32.HRP_STAKE_KEY_HASH, key_hash)) elif script_hash: return ("for script:", bech32.encode(bech32.HRP_SCRIPT_HASH, script_hash)) else: # should be unreachable unless there's a bug in validation raise ValueError async def confirm_cvote_registration_delegation( ctx: Context, public_key: str, weight: int, ) -> None: props: list[PropertyType] = [ ("Vote key registration (CIP-36)", None), ("Delegating to:", public_key), ] if weight is not None: props.append(("Weight:", str(weight))) await confirm_properties( ctx, "confirm_cvote_registration_delegation", title="Confirm transaction", props=props, br_code=ButtonRequestType.Other, ) async def confirm_cvote_registration_payment_address( ctx: Context, payment_address: str, should_show_payment_warning: bool, ) -> None: props = [ ("Vote key registration (CIP-36)", None), ("Rewards go to:", payment_address), ] if should_show_payment_warning: props.append((CVOTE_REWARD_ELIGIBILITY_WARNING, None)) await confirm_properties( ctx, "confirm_cvote_registration_payment_address", title="Confirm transaction", props=props, br_code=ButtonRequestType.Other, ) async def confirm_cvote_registration( ctx: Context, vote_public_key: str | None, staking_path: list[int], nonce: int, voting_purpose: int | None, ) -> None: props: list[PropertyType] = [("Vote key registration (CIP-36)", None)] if vote_public_key is not None: props.append(("Vote public key:", vote_public_key)) props.extend( [ ( f"Staking key for account {format_account_number(staking_path)}:", address_n_to_str(staking_path), ), ("Nonce:", str(nonce)), ] ) if voting_purpose is not None: props.append( ( "Voting purpose:", "Catalyst" if voting_purpose == 0 else f"{voting_purpose} (other)", ) ) await confirm_properties( ctx, "confirm_cvote_registration", title="Confirm transaction", props=props, br_code=ButtonRequestType.Other, ) async def show_auxiliary_data_hash(ctx: Context, auxiliary_data_hash: bytes) -> None: await confirm_properties( ctx, "confirm_auxiliary_data", "Confirm transaction", (("Auxiliary data hash:", auxiliary_data_hash),), br_code=BRT_Other, ) async def confirm_token_minting( ctx: Context, policy_id: bytes, token: messages.CardanoToken ) -> None: assert token.mint_amount is not None # _validate_token await confirm_properties( ctx, "confirm_mint", "Confirm transaction", ( ( "Asset fingerprint:", format_asset_fingerprint( policy_id, token.asset_name_bytes, ), ), ( "Amount minted:" if token.mint_amount >= 0 else "Amount burned:", format_amount(token.mint_amount, 0), ), ), br_code=BRT_Other, ) async def warn_tx_network_unverifiable(ctx: Context) -> None: await confirm_metadata( ctx, "warning_no_outputs", "Warning", "Transaction has no outputs, network cannot be verified.", br_code=BRT_Other, ) async def confirm_script_data_hash(ctx: Context, script_data_hash: bytes) -> None: await confirm_properties( ctx, "confirm_script_data_hash", "Confirm transaction", ( ( "Script data hash:", bech32.encode(bech32.HRP_SCRIPT_DATA_HASH, script_data_hash), ), ), br_code=BRT_Other, ) async def confirm_collateral_input( ctx: Context, collateral_input: messages.CardanoTxCollateralInput ) -> None: await confirm_properties( ctx, "confirm_collateral_input", "Confirm transaction", ( ("Collateral input ID:", collateral_input.prev_hash), ("Collateral input index:", str(collateral_input.prev_index)), ), br_code=BRT_Other, ) async def confirm_reference_input( ctx: Context, reference_input: messages.CardanoTxReferenceInput ) -> None: await confirm_properties( ctx, "confirm_reference_input", "Confirm transaction", ( ("Reference input ID:", reference_input.prev_hash), ("Reference input index:", str(reference_input.prev_index)), ), br_code=BRT_Other, ) async def confirm_required_signer( ctx: Context, required_signer: messages.CardanoTxRequiredSigner ) -> None: assert ( required_signer.key_hash is not None or required_signer.key_path ) # _validate_required_signer formatted_signer = ( bech32.encode(bech32.HRP_REQUIRED_SIGNER_KEY_HASH, required_signer.key_hash) if required_signer.key_hash is not None else address_n_to_str(required_signer.key_path) ) await confirm_properties( ctx, "confirm_required_signer", "Confirm transaction", (("Required signer", formatted_signer),), br_code=BRT_Other, ) async def show_cardano_address( ctx: Context, address_parameters: messages.CardanoAddressParametersType, address: str, protocol_magic: int, ) -> None: CAT = CardanoAddressType # local_cache_global network_name = None if not protocol_magics.is_mainnet(protocol_magic): network_name = protocol_magics.to_ui_string(protocol_magic) title = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]} address" address_extra = None title_qr = title if address_parameters.address_type in ( CAT.BYRON, CAT.BASE, CAT.BASE_KEY_SCRIPT, CAT.POINTER, CAT.ENTERPRISE, CAT.REWARD, ): if address_parameters.address_n: address_extra = address_n_to_str(address_parameters.address_n) title_qr = address_n_to_str(address_parameters.address_n) elif address_parameters.address_n_staking: address_extra = address_n_to_str(address_parameters.address_n_staking) title_qr = address_n_to_str(address_parameters.address_n_staking) await layouts.show_address( ctx, address, title=title, network=network_name, address_extra=address_extra, title_qr=title_qr, )