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/trezor/ui/layouts/tt/__init__.py

1123 lines
33 KiB

from micropython import const
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import ui, wire
from trezor.enums import ButtonRequestType
from trezor.ui.container import Container
from trezor.ui.loader import LoaderDanger
from trezor.ui.popup import Popup
from trezor.ui.qr import Qr
from trezor.utils import chunks, chunks_intersperse
from ...components.common import break_path_to_lines
from ...components.common.confirm import (
CONFIRMED,
GO_BACK,
SHOW_PAGINATED,
is_confirmed,
raise_if_cancelled,
)
from ...components.tt import passphrase, pin
from ...components.tt.button import ButtonCancel, ButtonDefault
from ...components.tt.confirm import Confirm, HoldToConfirm, InfoConfirm
from ...components.tt.scroll import (
PAGEBREAK,
AskPaginated,
Paginated,
paginate_paragraphs,
)
from ...components.tt.text import LINE_WIDTH_PAGINATED, Span, Text
from ...constants.tt import (
MONO_ADDR_PER_LINE,
MONO_HEX_PER_LINE,
QR_X,
QR_Y,
TEXT_MAX_LINES,
)
from ..common import button_request, interact
if TYPE_CHECKING:
from typing import Any, Awaitable, Iterable, Iterator, NoReturn, Sequence
from ..common import PropertyType, ExceptionType
from ...components.tt.button import ButtonContent
__all__ = (
"confirm_action",
"confirm_address",
"confirm_text",
"confirm_amount",
"confirm_reset_device",
"confirm_backup",
"confirm_path_warning",
"confirm_sign_identity",
"confirm_signverify",
"show_address",
"show_error_and_raise",
"show_pubkey",
"show_success",
"show_xpub",
"show_warning",
"confirm_output",
"confirm_payment_request",
"confirm_blob",
"confirm_properties",
"confirm_total",
"confirm_joint_total",
"confirm_metadata",
"confirm_replacement",
"confirm_modify_output",
"confirm_modify_fee",
"confirm_coinjoin",
"show_coinjoin",
"show_popup",
"draw_simple_text",
"request_passphrase_on_device",
"request_pin_on_device",
"should_show_more",
)
async def confirm_action(
ctx: wire.GenericContext,
br_type: str,
title: str,
action: str | None = None,
description: str | None = None,
description_param: str | None = None,
description_param_font: int = ui.BOLD,
verb: ButtonContent = Confirm.DEFAULT_CONFIRM,
verb_cancel: ButtonContent | None = Confirm.DEFAULT_CANCEL,
hold: bool = False,
hold_danger: bool = False,
icon: str | None = None, # TODO cleanup @ redesign
icon_color: int | None = None, # TODO cleanup @ redesign
reverse: bool = False, # TODO cleanup @ redesign
larger_vspace: bool = False, # TODO cleanup @ redesign
exc: ExceptionType = wire.ActionCancelled,
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
text = Text(
title,
icon if icon is not None else ui.ICON_DEFAULT,
icon_color if icon_color is not None else ui.ORANGE_ICON,
new_lines=False,
)
if reverse and description is not None:
text.format_parametrized(
description,
description_param if description_param is not None else "",
param_font=description_param_font,
)
elif action is not None:
text.bold(action)
if action is not None and description is not None:
text.br()
if larger_vspace:
text.br_half()
if reverse and action is not None:
text.bold(action)
elif description is not None:
text.format_parametrized(
description,
description_param if description_param is not None else "",
param_font=description_param_font,
)
layout: ui.Layout
if hold_danger:
assert isinstance(verb, str)
layout = HoldToConfirm(
text,
confirm=verb,
loader_style=LoaderDanger,
confirm_style=ButtonCancel,
cancel=verb_cancel is not None,
)
elif hold:
assert isinstance(verb, str)
layout = HoldToConfirm(text, confirm=verb, cancel=verb_cancel is not None)
else:
layout = Confirm(text, confirm=verb, cancel=verb_cancel)
await raise_if_cancelled(
interact(ctx, layout, br_type, br_code),
exc,
)
async def confirm_reset_device(
ctx: wire.GenericContext, prompt: str, recovery: bool = False
) -> None:
if recovery:
text = Text("Recovery mode", ui.ICON_RECOVERY, new_lines=False)
else:
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
text.bold(prompt)
text.br()
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to ")
text.bold("https://trezor.io/tos")
await raise_if_cancelled(
interact(
ctx,
Confirm(text, major_confirm=not recovery),
"recover_device" if recovery else "setup_device",
ButtonRequestType.ProtectCall
if recovery
else ButtonRequestType.ResetDevice,
)
)
# TODO cleanup @ redesign
async def confirm_backup(ctx: wire.GenericContext) -> bool:
text1 = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False)
text1.bold("New wallet created successfully!\n")
text1.br_half()
text1.normal("You should back up your new wallet right now.")
text2 = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False)
text2.bold("Are you sure you want to skip the backup?\n")
text2.br_half()
text2.normal("You can back up your Trezor once, at any time.")
if is_confirmed(
await interact(
ctx,
Confirm(text1, cancel="Skip", confirm="Back up", major_confirm=True),
"backup_device",
ButtonRequestType.ResetDevice,
)
):
return True
confirmed = is_confirmed(
await interact(
ctx,
Confirm(text2, cancel="Skip", confirm="Back up", major_confirm=True),
"backup_device",
ButtonRequestType.ResetDevice,
)
)
return confirmed
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_type)
text.mono(*break_path_to_lines(path, MONO_ADDR_PER_LINE))
text.normal("is unknown.", "Are you sure?")
await raise_if_cancelled(
interact(
ctx,
Confirm(text),
"path_warning",
ButtonRequestType.UnknownDerivationPath,
)
)
def _show_qr(
address: str,
case_sensitive: bool,
title: str,
cancel: str = "Address",
) -> Confirm:
qr = Qr(address, case_sensitive, QR_X, QR_Y)
text = Text(title, ui.ICON_RECEIVE, ui.GREEN)
return Confirm(Container(qr, text), cancel=cancel, cancel_style=ButtonDefault)
def _truncate_hex(
hex_data: str,
lines: int = TEXT_MAX_LINES,
width: int = MONO_HEX_PER_LINE,
middle: bool = False,
ellipsis: str = "...", # TODO: cleanup @ redesign
) -> Iterator[str]:
ell_len = len(ellipsis)
if len(hex_data) > width * lines:
if middle:
hex_data = (
hex_data[: lines * width // 2 - (ell_len // 2)]
+ ellipsis
+ hex_data[-lines * width // 2 + (ell_len - ell_len // 2) :]
)
else:
hex_data = hex_data[: (width * lines - ell_len)] + ellipsis
return chunks_intersperse(hex_data, width)
def _show_address(
address: str,
title: str,
network: str | None = None,
extra: str | None = None,
) -> ui.Layout:
para = [(ui.NORMAL, f"{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)
)
return paginate_paragraphs(
para,
header=title,
header_icon=ui.ICON_RECEIVE,
icon_color=ui.GREEN,
confirm=lambda content: Confirm(
content, cancel="QR", cancel_style=ButtonDefault
),
)
def _show_xpub(xpub: str, title: str, cancel: str) -> Paginated:
pages: list[ui.Component] = []
for lines in chunks(list(chunks_intersperse(xpub, 16)), TEXT_MAX_LINES * 2):
text = Text(title, ui.ICON_RECEIVE, ui.GREEN, new_lines=False)
text.mono(*lines)
pages.append(text)
content = Paginated(pages)
content.pages[-1] = Confirm(
content.pages[-1],
cancel=cancel,
cancel_style=ButtonDefault,
)
return content
async def show_xpub(
ctx: wire.GenericContext, xpub: str, title: str, cancel: str
) -> None:
await raise_if_cancelled(
interact(
ctx,
_show_xpub(xpub, title, cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
)
async def show_address(
ctx: wire.GenericContext,
address: str,
*,
address_qr: str | None = None,
case_sensitive: bool = True,
title: str = "Confirm 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:
if is_confirmed(
await interact(
ctx,
_show_address(
address,
title,
network,
extra=address_extra,
),
"show_address",
ButtonRequestType.Address,
)
):
break
if is_confirmed(
await interact(
ctx,
_show_qr(
address if address_qr is None else address_qr,
case_sensitive,
title if title_qr is None else title_qr,
cancel="XPUBs" if is_multisig else "Address",
),
"show_qr",
ButtonRequestType.Address,
)
):
break
if is_multisig:
for i, xpub in enumerate(xpubs):
cancel = "Next" if i < len(xpubs) - 1 else "Address"
title_xpub = f"XPUB #{i + 1}"
title_xpub += " (yours)" if i == multisig_index else " (cosigner)"
if is_confirmed(
await interact(
ctx,
_show_xpub(xpub, title=title_xpub, cancel=cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
):
return
def show_pubkey(
ctx: wire.Context, pubkey: str, title: str = "Confirm public key"
) -> Awaitable[None]:
return confirm_blob(
ctx,
br_type="show_pubkey",
title="Confirm public key",
data=pubkey,
br_code=ButtonRequestType.PublicKey,
icon=ui.ICON_RECEIVE,
)
async def _show_modal(
ctx: wire.GenericContext,
br_type: str,
br_code: ButtonRequestType,
header: str,
subheader: str | None,
content: str,
button_confirm: str | None,
button_cancel: str | None,
icon: str,
icon_color: int,
exc: ExceptionType = wire.ActionCancelled,
) -> None:
text = Text(header, icon, icon_color, new_lines=False)
if subheader:
text.bold(subheader)
text.br()
text.br_half()
text.normal(content)
await raise_if_cancelled(
interact(
ctx,
Confirm(text, confirm=button_confirm, cancel=button_cancel),
br_type,
br_code,
),
exc,
)
async def show_error_and_raise(
ctx: wire.GenericContext,
br_type: str,
content: str,
header: str = "Error",
subheader: str | None = None,
button: str = "Close",
red: bool = False,
exc: ExceptionType = wire.ActionCancelled,
) -> NoReturn:
await _show_modal(
ctx,
br_type=br_type,
br_code=ButtonRequestType.Other,
header=header,
subheader=subheader,
content=content,
button_confirm=None,
button_cancel=button,
icon=ui.ICON_WRONG,
icon_color=ui.RED if red else ui.ORANGE_ICON,
exc=exc,
)
raise exc
def show_warning(
ctx: wire.GenericContext,
br_type: str,
content: str,
header: str = "Warning",
subheader: str | None = None,
button: str = "Try again",
br_code: ButtonRequestType = ButtonRequestType.Warning,
icon: str = ui.ICON_WRONG,
icon_color: int = ui.RED,
) -> Awaitable[None]:
return _show_modal(
ctx,
br_type=br_type,
br_code=br_code,
header=header,
subheader=subheader,
content=content,
button_confirm=button,
button_cancel=None,
icon=icon,
icon_color=icon_color,
)
def show_success(
ctx: wire.GenericContext,
br_type: str,
content: str,
subheader: str | None = None,
button: str = "Continue",
) -> Awaitable[None]:
return _show_modal(
ctx,
br_type=br_type,
br_code=ButtonRequestType.Success,
header="Success",
subheader=subheader,
content=content,
button_confirm=button,
button_cancel=None,
icon=ui.ICON_CONFIRM,
icon_color=ui.GREEN,
)
async def confirm_output(
ctx: wire.GenericContext,
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,
icon: str = ui.ICON_SEND,
) -> None:
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, icon, ui.GREEN)
else:
text = Text(title, icon, 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)
await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code))
async def confirm_payment_request(
ctx: wire.GenericContext,
recipient_name: str,
amount: str,
memos: list[str],
) -> Any:
para = [(ui.NORMAL, f"{amount} to\n{recipient_name}")]
para.extend((ui.NORMAL, memo) for memo in memos)
content = paginate_paragraphs(
para,
"Confirm sending",
ui.ICON_SEND,
ui.GREEN,
confirm=lambda text: InfoConfirm(text, info="Details"),
)
return await raise_if_cancelled(
interact(
ctx, content, "confirm_payment_request", ButtonRequestType.ConfirmOutput
)
)
async def should_show_more(
ctx: wire.GenericContext,
title: str,
para: Iterable[tuple[int, str]],
button_text: str = "Show all",
br_type: str = "should_show_more",
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_DEFAULT,
icon_color: int = ui.ORANGE_ICON,
confirm: str | bytes | None = None,
major_confirm: bool = False,
) -> bool:
"""Return True if the user wants to show more (they click a special button)
and False when the user wants to continue without showing details.
Raises ActionCancelled if the user cancels.
"""
if confirm is None:
confirm = Confirm.DEFAULT_CONFIRM
page = Text(
title,
header_icon=icon,
icon_color=icon_color,
new_lines=False,
max_lines=TEXT_MAX_LINES - 2,
)
for font, text in para:
page.content.extend((font, text, "\n"))
ask_dialog = Confirm(
AskPaginated(page, button_text),
confirm=confirm,
major_confirm=major_confirm,
)
result = await raise_if_cancelled(interact(ctx, ask_dialog, br_type, br_code))
assert result in (SHOW_PAGINATED, CONFIRMED)
return result is SHOW_PAGINATED
async def _confirm_ask_pagination(
ctx: wire.GenericContext,
br_type: str,
title: str,
para: Iterable[tuple[int, str]],
para_truncated: Iterable[tuple[int, str]],
br_code: ButtonRequestType,
icon: str,
icon_color: int,
) -> None:
paginated: ui.Layout | None = None
while True:
if not await should_show_more(
ctx,
title,
para=para_truncated,
br_type=br_type,
br_code=br_code,
icon=icon,
icon_color=icon_color,
):
return
if paginated is None:
paginated = paginate_paragraphs(
para,
header=None,
back_button=True,
confirm=lambda content: Confirm(
content, cancel=None, confirm="Close", confirm_style=ButtonDefault
),
)
result = await interact(ctx, paginated, br_type, br_code)
assert result in (CONFIRMED, GO_BACK)
assert False
async def confirm_blob(
ctx: wire.GenericContext,
br_type: str,
title: str,
data: bytes | str,
description: str | None = None,
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
ask_pagination: bool = False,
) -> None:
"""Confirm data blob.
Applicable for public keys, signatures, hashes. In general, any kind of
data that is not human-readable, and can be wrapped at any character.
For addresses, use `confirm_address`.
Displays in monospace font. Paginates automatically.
If data is provided as bytes or bytearray, it is converted to hex.
"""
if isinstance(data, (bytes, bytearray)):
data_str = hexlify(data).decode()
else:
data_str = data
span = Span()
lines = 0
if description is not None:
span.reset(description, 0, ui.NORMAL)
lines += span.count_lines()
data_lines = (len(data_str) + MONO_HEX_PER_LINE - 1) // MONO_HEX_PER_LINE
lines += data_lines
if lines <= TEXT_MAX_LINES:
text = Text(title, icon, icon_color, new_lines=False)
if description is not None:
text.normal(description)
text.br()
# special case:
if len(data_str) % 16 == 0:
# sanity checks:
# (a) we must not exceed MONO_HEX_PER_LINE
assert MONO_HEX_PER_LINE > 16
# (b) we must not increase number of lines
assert (len(data_str) // 16) <= data_lines
# the above holds true for MONO_HEX_PER_LINE == 18 and TEXT_MAX_LINES == 5
per_line = 16
else:
per_line = MONO_HEX_PER_LINE
text.mono(ui.FG, *chunks_intersperse(data_str, per_line))
content: ui.Layout = HoldToConfirm(text) if hold else Confirm(text)
return await raise_if_cancelled(interact(ctx, content, br_type, br_code))
elif ask_pagination:
para = [(ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2)]
para_truncated = []
if description is not None:
para_truncated.append((ui.NORMAL, description))
para_truncated.extend(para[:TEXT_MAX_LINES])
return await _confirm_ask_pagination(
ctx, br_type, title, para, para_truncated, br_code, icon, icon_color
)
else:
para = []
if description is not None:
para.append((ui.NORMAL, description))
para.extend((ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2))
paginated = paginate_paragraphs(
para, title, icon, icon_color, confirm=HoldToConfirm if hold else Confirm
)
return await raise_if_cancelled(interact(ctx, paginated, br_type, br_code))
def confirm_address(
ctx: wire.GenericContext,
title: str,
address: str,
description: str | None = "Address:",
br_type: str = "confirm_address",
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
) -> Awaitable[None]:
# TODO clarify API - this should be pretty limited to support mainly confirming
# destinations and similar
return confirm_blob(
ctx,
br_type=br_type,
title=title,
data=address,
description=description,
br_code=br_code,
icon=icon,
icon_color=icon_color,
)
async def confirm_text(
ctx: wire.GenericContext,
br_type: str,
title: str,
data: str,
description: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
) -> None:
"""Confirm textual data.
Applicable for human-readable strings, numbers, date/time values etc.
For amounts, use `confirm_amount`.
Displays in bold font. Paginates automatically.
"""
span = Span()
lines = 0
if description is not None:
span.reset(description, 0, ui.NORMAL)
lines += span.count_lines()
span.reset(data, 0, ui.BOLD)
lines += span.count_lines()
if lines <= TEXT_MAX_LINES:
text = Text(title, icon, icon_color, new_lines=False)
if description is not None:
text.normal(description)
text.br()
text.bold(data)
content: ui.Layout = Confirm(text)
else:
para = []
if description is not None:
para.append((ui.NORMAL, description))
para.append((ui.BOLD, data))
content = paginate_paragraphs(para, title, icon, icon_color)
await raise_if_cancelled(interact(ctx, content, br_type, br_code))
def confirm_amount(
ctx: wire.GenericContext,
title: str,
amount: str,
description: str = "Amount:",
br_type: str = "confirm_amount",
br_code: ButtonRequestType = ButtonRequestType.Other,
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
) -> Awaitable[None]:
"""Confirm amount."""
# TODO clarify API - this should be pretty limited to support mainly confirming
# destinations and similar
return confirm_text(
ctx,
br_type=br_type,
title=title,
data=amount,
description=description,
br_code=br_code,
icon=icon,
icon_color=icon_color,
)
_SCREEN_FULL_THRESHOLD = const(2)
# TODO keep name and value on the same page if possible
async def confirm_properties(
ctx: wire.GenericContext,
br_type: str,
title: str,
props: Iterable[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:
span = Span()
para = []
used_lines = 0
for key, val in props:
span.reset(key or "", 0, ui.NORMAL, line_width=LINE_WIDTH_PAGINATED)
key_lines = span.count_lines()
if isinstance(val, str):
span.reset(val, 0, ui.BOLD, line_width=LINE_WIDTH_PAGINATED)
val_lines = span.count_lines()
elif isinstance(val, bytes):
val_lines = (len(val) * 2 + MONO_HEX_PER_LINE - 1) // MONO_HEX_PER_LINE
else:
val_lines = 0
remaining_lines = TEXT_MAX_LINES - used_lines
used_lines = (used_lines + key_lines + val_lines) % TEXT_MAX_LINES
if key_lines + val_lines > remaining_lines:
if remaining_lines <= _SCREEN_FULL_THRESHOLD:
# there are only 2 remaining lines, don't try to fit and put everything
# on next page
para.append(PAGEBREAK)
used_lines = (key_lines + val_lines) % TEXT_MAX_LINES
elif val_lines > 0 and key_lines >= remaining_lines:
# more than 2 remaining lines so try to fit something -- but won't fit
# at least one line of value
para.append(PAGEBREAK)
used_lines = (key_lines + val_lines) % TEXT_MAX_LINES
elif key_lines + val_lines <= TEXT_MAX_LINES:
# Whole property won't fit to the page, but it will fit on a page
# by itself
para.append(PAGEBREAK)
used_lines = (key_lines + val_lines) % TEXT_MAX_LINES
# else:
# None of the above. Continue fitting on the same page.
if key:
para.append((ui.NORMAL, key))
if isinstance(val, bytes):
para.extend(
(ui.MONO, line)
for line in chunks(hexlify(val).decode(), MONO_HEX_PER_LINE - 2)
)
elif isinstance(val, str):
para.append((ui.BOLD, val))
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,
fee_amount: str,
fee_rate_amount: str | None = None,
title: str = "Confirm transaction",
total_label: str = "Total amount:\n",
fee_label: str = "\nincluding fee:\n",
icon_color: int = ui.GREEN,
br_type: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None:
text = Text(title, ui.ICON_SEND, icon_color, new_lines=False)
text.normal(total_label)
text.bold(total_amount)
text.normal(fee_label)
text.bold(fee_amount)
if fee_rate_amount is not None:
text.normal(f"\n({fee_rate_amount})")
await raise_if_cancelled(interact(ctx, HoldToConfirm(text), br_type, br_code))
async def confirm_joint_total(
ctx: wire.GenericContext, spending_amount: str, total_amount: str
) -> None:
text = Text("Joint transaction", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.normal("You are contributing:\n")
text.bold(spending_amount)
text.normal("\nto the total amount:\n")
text.bold(total_amount)
await raise_if_cancelled(
interact(
ctx, HoldToConfirm(text), "confirm_joint_total", ButtonRequestType.SignTx
)
)
async def confirm_metadata(
ctx: wire.GenericContext,
br_type: str,
title: str,
content: str,
param: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.SignTx,
hide_continue: bool = False,
hold: bool = False,
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(
content, param if param is not None else "", param_font=param_font
)
if not hide_continue:
text.br()
if larger_vspace:
text.br_half()
text.normal("Continue?")
cls = HoldToConfirm if hold else Confirm
await raise_if_cancelled(interact(ctx, cls(text), br_type, br_code))
async def confirm_replacement(
ctx: wire.GenericContext, description: str, txid: str
) -> None:
text = Text(description, ui.ICON_SEND, ui.GREEN, new_lines=False)
text.normal("Confirm transaction ID:\n")
text.mono(*_truncate_hex(txid, TEXT_MAX_LINES - 1))
await raise_if_cancelled(
interact(ctx, Confirm(text), "confirm_replacement", ButtonRequestType.SignTx)
)
async def confirm_modify_output(
ctx: wire.GenericContext,
address: str,
sign: int,
amount_change: str,
amount_new: str,
) -> None:
page1 = Text("Modify amount", ui.ICON_SEND, ui.GREEN, new_lines=False)
page1.normal("Address:\n")
page1.br_half()
page1.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE))
page2 = Text("Modify amount", ui.ICON_SEND, ui.GREEN, new_lines=False)
if sign < 0:
page2.normal("Decrease amount by:\n")
else:
page2.normal("Increase amount by:\n")
page2.bold(amount_change)
page2.br_half()
page2.normal("\nNew amount:\n")
page2.bold(amount_new)
await raise_if_cancelled(
interact(
ctx,
Paginated([page1, Confirm(page2)]),
"modify_output",
ButtonRequestType.ConfirmOutput,
)
)
async def confirm_modify_fee(
ctx: wire.GenericContext,
sign: int,
user_fee_change: str,
total_fee_new: str,
fee_rate_amount: str | None = None,
) -> None:
text = Text("Modify fee", ui.ICON_SEND, ui.GREEN, new_lines=False)
if sign == 0:
text.normal("Your fee did not change.\n")
else:
if sign < 0:
text.normal("Decrease your fee by:\n")
else:
text.normal("Increase your fee by:\n")
text.bold(user_fee_change)
text.br()
text.normal("Transaction fee:\n")
text.bold(total_fee_new)
if fee_rate_amount is not None:
text.normal(f"\n({fee_rate_amount})")
await raise_if_cancelled(
interact(ctx, HoldToConfirm(text), "modify_fee", ButtonRequestType.SignTx)
)
async def confirm_coinjoin(
ctx: wire.GenericContext, max_rounds: int, max_fee_per_vbyte: str
) -> None:
text = Text("Authorize CoinJoin", ui.ICON_RECOVERY, new_lines=False)
text.normal("Maximum rounds: ")
text.bold(f"{max_rounds}\n")
text.br_half()
text.normal("Maximum mining fee:\n")
text.bold(max_fee_per_vbyte)
await raise_if_cancelled(
interact(ctx, HoldToConfirm(text), "coinjoin_final", ButtonRequestType.Other)
)
def show_coinjoin() -> None:
text = Text("Please wait", ui.ICON_CONFIG, ui.RED)
text.normal("CoinJoin in progress.")
text.br()
text.bold("Do not disconnect your Trezor.")
ui.draw_simple(text)
# TODO cleanup @ redesign
async def confirm_sign_identity(
ctx: wire.GenericContext, proto: str, identity: str, challenge_visual: str | None
) -> None:
text = Text(f"Sign {proto}", new_lines=False)
if challenge_visual:
text.normal(challenge_visual)
text.br()
text.mono(*chunks_intersperse(identity, 18))
await raise_if_cancelled(
interact(ctx, Confirm(text), "sign_identity", ButtonRequestType.Other)
)
async def confirm_signverify(
ctx: wire.GenericContext, coin: str, message: str, address: str, verify: bool
) -> None:
if verify:
header = f"Verify {coin} message"
br_type = "verify_message"
else:
header = f"Sign {coin} message"
br_type = "sign_message"
text = Text(header, new_lines=False)
text.bold("Confirm address:\n")
text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE))
await raise_if_cancelled(
interact(ctx, Confirm(text), br_type, ButtonRequestType.Other)
)
para = [(ui.BOLD, "Confirm message:"), (ui.MONO, message)]
content = paginate_paragraphs(para, header)
await raise_if_cancelled(interact(ctx, content, br_type, ButtonRequestType.Other))
async def show_popup(
title: str,
description: str,
subtitle: str | None = None,
description_param: str = "",
timeout_ms: int = 3000,
) -> None:
text = Text(title, ui.ICON_WRONG, ui.RED)
if subtitle is not None:
text.bold(subtitle)
text.br_half()
text.format_parametrized(description, description_param)
await Popup(text, timeout_ms)
def draw_simple_text(title: str, description: str = "") -> None:
text = Text(title, ui.ICON_CONFIG, new_lines=False)
text.normal(description)
ui.draw_simple(text)
async def request_passphrase_on_device(ctx: wire.GenericContext, max_len: int) -> str:
await button_request(
ctx, "passphrase_device", code=ButtonRequestType.PassphraseEntry
)
keyboard = passphrase.PassphraseKeyboard("Enter passphrase", max_len)
result = await ctx.wait(keyboard)
if result is passphrase.CANCELLED:
raise wire.ActionCancelled("Passphrase entry cancelled")
assert isinstance(result, str)
return result
async def request_pin_on_device(
ctx: wire.GenericContext,
prompt: str,
attempts_remaining: int | None,
allow_cancel: bool,
) -> str:
await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry)
if attempts_remaining is None:
subprompt = None
elif attempts_remaining == 1:
subprompt = "This is your last attempt"
else:
subprompt = f"{attempts_remaining} attempts remaining"
dialog = pin.PinDialog(prompt, subprompt, allow_cancel)
while True:
result = await ctx.wait(dialog)
if result is pin.CANCELLED:
raise wire.PinCancelled
assert isinstance(result, str)
return result