|
|
|
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_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]],
|
feat(core/ethereum): EIP-712
Based on original contribution by Max Kupriianov <xlab@hey.com>
Implemented EIP-712 typed data signatures in Ethereum app.
Add eth_abi into pyproject deps
device test for EIP 712
fixed hex decoding for address
fixup! fixed hex decoding for address
code quality, more pythonic code, removing unused imports
running black and isort on changed files
trezorctl file input for EIP 712 data signing
fixup! code quality, more pythonic code, removing unused imports
fixup! fixup! code quality, more pythonic code, removing unused imports
necessary changes after rebase to master
unit tests for sign_typed_data.py
new protobuf messages, working for nonarray types
simplified and verified solution for our simple data
support for simple arrays, without their confirmation
reverting protobuf value messages to bytes, appropriate changes
showing arrays in Trezor, code quality improvements
data validation on Trezor, minor improvements
using custom types for storing type data instead of dicts, addressing feedback from review
moving helper functions to its own file, tests for decode_data
additional overall tests
support for arrays of structs
adding support for metamask_v4_compat variable
using HashWriter object to collect the final hash continously
minor improvements in code quality
validate_field_type function
streaming values from client without saving them, missing UI
prototype of streamed UI using confirm_properties
accounting for bytes in data, more data types in integration tests
rebase on master, using f-strings
minor fixes and improvements from code review
StructHasher class for the whole hashing process
mypy and style changes
asking users whether to show structs and arrays
protobuf descriptions to fix make defs_check
unifying comments, mypy fix
unit tests for StructHasher class
UI fixtures, skipping device tests for T1
addressing majority of code review comments about code quality and structure
changing file structure - layouts, helpers, sign_typed_data
decode_data renaming and docstring, renaming unit test file
using tuples instead of lists in elifs
layout improvements
excluding core/src/apps/common/confirm.py file from the PR
True/False returning layout with Show more button
code review layout improvements
forgotten br_type argument to should_show_more
4 years ago
|
|
|
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,
|
|
|
|
) -> 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.
|
|
|
|
"""
|
|
|
|
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))
|
|
|
|
|
|
|
|
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("\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,
|
|
|
|
) -> 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.br_half()
|
|
|
|
text.normal("Transaction fee:\n")
|
|
|
|
text.bold(total_fee_new)
|
|
|
|
await raise_if_cancelled(
|
|
|
|
interact(ctx, HoldToConfirm(text), "modify_fee", ButtonRequestType.SignTx)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def confirm_coinjoin(
|
|
|
|
ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str
|
|
|
|
) -> None:
|
|
|
|
text = Text("Authorize CoinJoin", ui.ICON_RECOVERY, new_lines=False)
|
|
|
|
text.normal("Coin name: ")
|
|
|
|
text.bold(f"{coin_name}\n")
|
|
|
|
text.br_half()
|
|
|
|
text.normal("Maximum rounds: ")
|
|
|
|
text.bold(f"{max_rounds}\n")
|
|
|
|
text.br_half()
|
|
|
|
text.normal("Maximum mining fee:\n")
|
|
|
|
text.bold(f"{max_fee_per_vbyte} sats/vbyte")
|
|
|
|
await raise_if_cancelled(
|
|
|
|
interact(ctx, HoldToConfirm(text), "coinjoin_final", ButtonRequestType.Other)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|