You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/apps/cardano/layout.py

529 lines
17 KiB

import math
from ubinascii import hexlify
from trezor import ui
from trezor.messages 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 apps.common.confirm import confirm, require_confirm, require_hold_to_confirm
from apps.common.layout import address_n_to_str
from . import seed
from .address import (
encode_human_readable_address,
get_public_key_hash,
pack_reward_address_bytes,
)
from .helpers import protocol_magics
from .helpers.utils import format_account_number, format_optional_int, to_account_path
if False:
from typing import List, Optional
from trezor import wire
from trezor.messages.CardanoBlockchainPointerType import (
CardanoBlockchainPointerType,
)
from trezor.messages.CardanoTxCertificateType import CardanoTxCertificateType
from trezor.messages.CardanoTxWithdrawalType import CardanoTxWithdrawalType
from trezor.messages.CardanoPoolParametersType import CardanoPoolParametersType
from trezor.messages.CardanoPoolOwnerType import CardanoPoolOwnerType
from trezor.messages.CardanoPoolMetadataType import CardanoPoolMetadataType
from trezor.messages.CardanoAssetGroupType import CardanoAssetGroupType
from trezor.messages.CardanoTokenType import CardanoTokenType
from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType
ADDRESS_TYPE_NAMES = {
CardanoAddressType.BYRON: "Legacy",
CardanoAddressType.BASE: "Base",
CardanoAddressType.POINTER: "Pointer",
CardanoAddressType.ENTERPRISE: "Enterprise",
CardanoAddressType.REWARD: "Reward",
}
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",
}
# 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")
def is_printable_ascii_bytestring(bytestr: bytes) -> bool:
return all((32 < b < 127) for b in bytestr)
async def confirm_sending(
ctx: wire.Context,
ada_amount: int,
token_bundle: List[CardanoAssetGroupType],
to: str,
) -> None:
for token_group in token_bundle:
await confirm_sending_token_group(ctx, token_group)
page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
page1.normal("Confirm sending:")
page1.bold(format_coin_amount(ada_amount))
page1.normal("to")
to_lines = list(chunks(to, 17))
page1.bold(to_lines[0])
comp: ui.Component = page1 # otherwise `[page1]` is of the wrong type
pages = [comp] + _paginate_lines(to_lines, 1, "Confirm transaction", ui.ICON_SEND)
await require_confirm(ctx, Paginated(pages))
async def confirm_sending_token_group(
ctx: wire.Context, token_group: CardanoAssetGroupType
) -> None:
page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
page1.bold("Policy id: ")
page1.mono(hexlify(token_group.policy_id).decode())
await require_confirm(ctx, page1)
for token_number, token in enumerate(token_group.tokens, 1):
if is_printable_ascii_bytestring(token.asset_name_bytes):
await confirm_sending_token_ascii(ctx, token, token_number)
else:
await confirm_sending_token_hex(ctx, token, token_number)
async def confirm_sending_token_ascii(
ctx: wire.Context, token: CardanoTokenType, token_number: int
) -> None:
page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
page1.normal("Asset #%s name (ASCII):" % (token_number))
page1.bold(token.asset_name_bytes.decode("ascii"))
page1.normal("Amount sent:")
page1.bold(format_amount(token.amount, 0))
await require_confirm(ctx, page1)
async def confirm_sending_token_hex(
ctx: wire.Context, token: CardanoTokenType, token_number: int
) -> None:
page1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
page1.bold("Asset #%s name (hex):" % (token_number))
page1.mono(hexlify(token.asset_name_bytes).decode())
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)
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)
async def show_warning_tx_no_staking_info(
ctx: wire.Context, address_type: EnumTypeCardanoAddressType, 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)
async def show_warning_tx_pointer_address(
ctx: wire.Context,
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)
)
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(
ctx: wire.Context,
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]))
async def show_warning_tx_staking_key_hash(
ctx: wire.Context,
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.")
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]))
async def confirm_transaction(
ctx: wire.Context,
amount: int,
fee: int,
protocol_magic: int,
ttl: Optional[int],
validity_interval_start: Optional[int],
has_metadata: bool,
is_network_id_verifiable: bool,
) -> None:
pages: List[ui.Component] = []
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)
if has_metadata:
page3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
page3.normal("Transaction contains")
page3.normal("metadata")
pages.append(page3)
await require_hold_to_confirm(ctx, Paginated(pages))
async def confirm_certificate(
ctx: wire.Context, certificate: CardanoTxCertificateType
) -> None:
# stake pool registration requires custom confirmation logic not covered
# 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)
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(hexlify(certificate.pool).decode())
pages.append(page2)
await require_confirm(ctx, Paginated(pages))
async def confirm_stake_pool_parameters(
ctx: wire.Context,
pool_parameters: CardanoPoolParametersType,
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(hexlify(pool_parameters.pool_id).decode())
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]))
async def confirm_stake_pool_owners(
ctx: wire.Context,
keychain: seed.Keychain,
owners: List[CardanoPoolOwnerType],
network_id: int,
) -> None:
pages: List[ui.Component] = []
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,
)
)
)
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)
)
)
pages.append(page)
await require_confirm(ctx, Paginated(pages))
async def confirm_stake_pool_metadata(
ctx: wire.Context,
metadata: Optional[CardanoPoolMetadataType],
) -> 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)
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]))
async def confirm_transaction_network_ttl(
ctx: wire.Context,
protocol_magic: int,
ttl: Optional[int],
validity_interval_start: Optional[int],
) -> 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)
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)
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)
async def show_address(
ctx: wire.Context,
address: str,
address_type: EnumTypeCardanoAddressType,
path: List[int],
network: str = 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,
)
address_lines = list(chunks(address, 17))
for address_line in address_lines[: lines_per_page - lines_used_on_first_page]:
page1.bold(address_line)
pages: List[ui.Component] = []
pages.append(page1)
# append remaining pages containing the rest of the address
pages.extend(
_paginate_lines(
address_lines,
lines_per_page - lines_used_on_first_page,
address_type_label,
ui.ICON_RECEIVE,
lines_per_page,
)
)
return await confirm(
ctx,
Paginated(pages),
code=ButtonRequestType.Address,
cancel="QR",
cancel_style=ButtonDefault,
)
def _paginate_lines(
lines: List[str], offset: int, desc: str, icon: str, lines_per_page: int = 4
) -> List[ui.Component]:
pages: List[ui.Component] = []
if len(lines) > offset:
to_pages = list(chunks(lines[offset:], lines_per_page))
for page in to_pages:
t = Text(desc, 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: Optional[bytes],
) -> 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))
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()
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?")
await require_confirm(ctx, Paginated([page1, page2]))
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)
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)