1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

refactor(core): introduce layouts

Layouts can be used by the application code to interact with user using
small number of dialogs or other groups of UI components. Each layout is
identified by name and takes some parameters. Most layouts will have an
implementation for each hardware model, mechanism is provided to import
the correct version so that application code can be oblivious to the
model.

This commit introduces the layout concept and converts a couple of
dialogs to use it.
This commit is contained in:
Martin Milata 2020-12-15 11:43:57 +01:00
parent 18cb429610
commit f38abf9d89
17 changed files with 565 additions and 257 deletions

View File

@ -495,12 +495,16 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
if TREZOR_MODEL == 'T':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py'))
elif TREZOR_MODEL == '1':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
else:
raise ValueError('Unknown Trezor model')

View File

@ -446,12 +446,16 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
if TREZOR_MODEL == 'T':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py'))
elif TREZOR_MODEL == '1':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py'))
else:
raise ValueError('Unknown Trezor model')

View File

@ -129,11 +129,12 @@ async def handle_EndSession(ctx: wire.Context, msg: EndSession) -> Success:
async def handle_Ping(ctx: wire.Context, msg: Ping) -> Success:
if msg.button_protection:
from apps.common.confirm import require_confirm
from trezor.ui.layouts import require, confirm_action
from trezor.messages.ButtonRequestType import ProtectCall
from trezor.ui.components.tt.text import Text
await require_confirm(ctx, Text("Confirm"), ProtectCall)
await require(
confirm_action(ctx, "ping", "Confirm", "ping", br_code=ProtectCall)
)
return Success(message=msg.message)

View File

@ -1,8 +1,9 @@
from trezor.crypto import bip32
from trezor.messages import InputScriptType
from trezor.messages.Address import Address
from trezor.ui.layouts import show_address
from apps.common.layout import address_n_to_str, show_address, show_qr, show_xpub
from apps.common.layout import address_n_to_str
from apps.common.paths import validate_path
from . import addresses
@ -18,15 +19,11 @@ if False:
from apps.common.coininfo import CoinInfo
async def show_xpubs(
ctx: wire.Context,
coin: CoinInfo,
xpub_magic: int,
pubnodes: List[HDNodeType],
multisig_index: int,
) -> bool:
for i, pubnode in enumerate(pubnodes):
cancel = "Next" if i < len(pubnodes) - 1 else "Address"
def _get_xpubs(
coin: CoinInfo, xpub_magic: int, pubnodes: List[HDNodeType]
) -> List[str]:
result = []
for pubnode in pubnodes:
node = bip32.HDNode(
depth=pubnode.depth,
fingerprint=pubnode.fingerprint,
@ -35,12 +32,9 @@ async def show_xpubs(
public_key=pubnode.public_key,
curve_name=coin.curve_name,
)
xpub = node.serialize_public(xpub_magic)
desc = "XPUB #%d" % (i + 1)
desc += " (yours)" if i == multisig_index else " (cosigner)"
if await show_xpub(ctx, xpub, desc=desc, cancel=cancel):
return True
return False
result.append(node.serialize_public(xpub_magic))
return result
@with_keychain
@ -88,22 +82,20 @@ async def get_address(
else:
pubnodes = [hd.node for hd in msg.multisig.pubkeys]
multisig_index = multisig_pubkey_index(msg.multisig, node.public_key())
desc = "Multisig %d of %d" % (msg.multisig.m, len(pubnodes))
while True:
if await show_address(ctx, address_short, desc=desc):
break
if await show_qr(ctx, address_qr, desc=desc, cancel="XPUBs"):
break
if await show_xpubs(
ctx, coin, multisig_xpub_magic, pubnodes, multisig_index
):
break
await show_address(
ctx,
address=address_short,
address_qr=address_qr,
desc=desc,
multisig_index=multisig_index,
xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes),
)
else:
desc = address_n_to_str(msg.address_n)
while True:
if await show_address(ctx, address_short, desc=desc):
break
if await show_qr(ctx, address_qr, desc=desc):
break
await show_address(
ctx, address=address_short, address_qr=address_qr, desc=desc
)
return Address(address=address)

View File

@ -1,22 +1,21 @@
from micropython import const
from ubinascii import hexlify
from trezor import ui
from trezor.messages import AmountUnit, ButtonRequestType, OutputScriptType
from trezor.strings import format_amount
from trezor.ui.components.tt.text import Text
from trezor.utils import chunks
from apps.common.confirm import require_confirm, require_hold_to_confirm
from trezor.ui import layouts
from trezor.ui.layouts import require
from .. import addresses
from . import omni
if False:
from typing import Iterator
from typing import Optional
from trezor import wire
from trezor.messages.SignTx import EnumTypeAmountUnit
from trezor.messages.TxOutput import TxOutput
from trezor.ui.layouts import LayoutType
from apps.common.coininfo import CoinInfo
@ -40,14 +39,6 @@ def format_coin_amount(
return "%s %s" % (format_amount(amount, decimals), shortcut)
def split_address(address: str) -> Iterator[str]:
return chunks(address, 17)
def split_op_return(data: str) -> Iterator[str]:
return chunks(data, 18)
async def confirm_output(
ctx: wire.Context, output: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmountUnit
) -> None:
@ -56,33 +47,40 @@ async def confirm_output(
assert data is not None
if omni.is_valid(data):
# OMNI transaction
text = Text("OMNI transaction", ui.ICON_SEND, ui.GREEN)
text.normal(omni.parse(data))
layout: LayoutType = layouts.confirm_metadata(
ctx,
"omni_transaction",
"OMNI transaction",
omni.parse(data),
br_code=ButtonRequestType.ConfirmOutput,
)
else:
# generic OP_RETURN
hex_data = hexlify(data).decode()
if len(hex_data) >= 18 * 5:
hex_data = hex_data[: (18 * 5 - 3)] + "..."
text = Text("OP_RETURN", ui.ICON_SEND, ui.GREEN)
text.mono(*split_op_return(hex_data))
layout = layouts.confirm_hex(
ctx,
"op_return",
"OP_RETURN",
hexlify(data).decode(),
ButtonRequestType.ConfirmOutput,
)
else:
address = output.address
assert address is not None
address_short = addresses.address_short(coin, address)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.normal(format_coin_amount(output.amount, coin, amount_unit) + " to")
text.mono(*split_address(address_short))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
assert output.address is not None
address_short = addresses.address_short(coin, output.address)
layout = layouts.confirm_output(
ctx, address_short, format_coin_amount(output.amount, coin, amount_unit)
)
await require(layout)
async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes) -> None:
text = Text(description, ui.ICON_SEND, ui.GREEN)
text.normal("Confirm transaction ID:")
hex_data = hexlify(txid).decode()
if len(hex_data) >= 18 * 4:
hex_data = hex_data[: (18 * 4 - 3)] + "..."
text.mono(*split_op_return(hex_data))
await require_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_replacement(
ctx,
description,
hexlify(txid).decode(),
)
)
async def confirm_modify_fee(
@ -92,19 +90,14 @@ async def confirm_modify_fee(
coin: CoinInfo,
amount_unit: EnumTypeAmountUnit,
) -> None:
text = Text("Fee modification", ui.ICON_SEND, ui.GREEN)
if user_fee_change == 0:
text.normal("Your fee did not change.")
else:
if user_fee_change < 0:
text.normal("Decrease your fee by:")
else:
text.normal("Increase your fee by:")
text.bold(format_coin_amount(abs(user_fee_change), coin, amount_unit))
text.br_half()
text.normal("Transaction fee:")
text.bold(format_coin_amount(total_fee_new, coin, amount_unit))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_modify_fee(
ctx,
user_fee_change,
format_coin_amount(abs(user_fee_change), coin, amount_unit),
format_coin_amount(total_fee_new, coin, amount_unit),
)
)
async def confirm_joint_total(
@ -114,12 +107,13 @@ async def confirm_joint_total(
coin: CoinInfo,
amount_unit: EnumTypeAmountUnit,
) -> None:
text = Text("Joint transaction", ui.ICON_SEND, ui.GREEN)
text.normal("You are contributing:")
text.bold(format_coin_amount(spending, coin, amount_unit))
text.normal("to the total amount:")
text.bold(format_coin_amount(total, coin, amount_unit))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_joint_total(
ctx,
spending_amount=format_coin_amount(spending, coin, amount_unit),
total_amount=format_coin_amount(total, coin, amount_unit),
),
)
async def confirm_total(
@ -129,50 +123,69 @@ async def confirm_total(
coin: CoinInfo,
amount_unit: EnumTypeAmountUnit,
) -> None:
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal("Total amount:")
text.bold(format_coin_amount(spending, coin, amount_unit))
text.normal("including fee:")
text.bold(format_coin_amount(fee, coin, amount_unit))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_total(
ctx,
total_amount=format_coin_amount(spending, coin, amount_unit),
fee_amount=format_coin_amount(fee, coin, amount_unit),
),
)
async def confirm_feeoverthreshold(
ctx: wire.Context, fee: int, coin: CoinInfo, amount_unit: EnumTypeAmountUnit
) -> None:
text = Text("High fee", ui.ICON_SEND, ui.GREEN)
text.normal("The fee of")
text.bold(format_coin_amount(fee, coin, amount_unit))
text.normal("is unexpectedly high.", "Continue?")
await require_confirm(ctx, text, ButtonRequestType.FeeOverThreshold)
fee_amount = format_coin_amount(fee, coin, amount_unit)
await require(
layouts.confirm_metadata(
ctx,
"fee_over_threshold",
"High fee",
"The fee of\n{}is unexpectedly high.",
fee_amount,
ButtonRequestType.FeeOverThreshold,
)
)
async def confirm_change_count_over_threshold(
ctx: wire.Context, change_count: int
) -> None:
text = Text("Warning", ui.ICON_SEND, ui.GREEN)
text.normal("There are {}".format(change_count))
text.normal("change-outputs.")
text.br_half()
text.normal("Continue?")
await require_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_metadata(
ctx,
"change_count_over_threshold",
"Warning",
"There are {}\nchange-outputs.\n",
str(change_count),
ButtonRequestType.SignTx,
)
)
async def confirm_nondefault_locktime(
ctx: wire.Context, lock_time: int, lock_time_disabled: bool
) -> None:
if lock_time_disabled:
text = Text("Warning", ui.ICON_SEND, ui.GREEN)
text.normal("Locktime is set but will", "have no effect.")
text.br_half()
title = "Warning"
text = "Locktime is set but will\nhave no effect.\n"
param: Optional[str] = None
elif lock_time < _LOCKTIME_TIMESTAMP_MIN_VALUE:
title = "Confirm locktime"
text = "Locktime for this\ntransaction is set to\nblockheight:\n{}"
param = str(lock_time)
else:
text = Text("Confirm locktime", ui.ICON_SEND, ui.GREEN)
text.normal("Locktime for this", "transaction is set to")
if lock_time < _LOCKTIME_TIMESTAMP_MIN_VALUE:
text.normal("blockheight:")
else:
text.normal("timestamp:")
text.bold(str(lock_time))
title = "Confirm locktime"
text = "Locktime for this\ntransaction is set to\ntimestamp:\n{}"
param = str(lock_time)
text.normal("Continue?")
await require_confirm(ctx, text, ButtonRequestType.SignTx)
await require(
layouts.confirm_metadata(
ctx,
"nondefault_locktime",
title,
text,
param,
br_code=ButtonRequestType.SignTx,
)
)

View File

@ -69,27 +69,11 @@ async def show_pubkey(ctx: wire.Context, pubkey: bytes) -> None:
await require_confirm(ctx, text, ButtonRequestType.PublicKey)
async def show_xpub(ctx: wire.Context, xpub: str, desc: str, cancel: str) -> bool:
pages: List[ui.Component] = []
for lines in chunks(list(chunks(xpub, 16)), 5):
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
text.mono(*lines)
pages.append(text)
return await confirm(
ctx,
Paginated(pages),
code=ButtonRequestType.PublicKey,
cancel=cancel,
cancel_style=ButtonDefault,
)
def split_address(address: str) -> Iterator[str]:
return chunks(address, 17)
def address_n_to_str(address_n: list) -> str:
def address_n_to_str(address_n: Iterable[int]) -> str:
def path_item(i: int) -> str:
if i & HARDENED:
return str(i ^ HARDENED) + "'"

View File

@ -1,11 +1,8 @@
from micropython import const
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.components.tt.text import Text
from trezor.ui.constants import MONO_CHARS_PER_LINE
from trezor.ui.layouts import confirm_path_warning, require
from . import HARDENED
from .confirm import require_confirm
from .layout import address_n_to_str
if False:
from typing import (
@ -262,12 +259,7 @@ async def validate_path(
async def show_path_warning(ctx: wire.Context, path: Bip32Path) -> None:
text = Text("Confirm path", ui.ICON_WRONG, ui.RED)
text.normal("Path")
text.mono(*break_address_n_to_lines(path))
text.normal("is unknown.")
text.normal("Are you sure?")
await require_confirm(ctx, text, ButtonRequestType.UnknownDerivationPath)
await require(confirm_path_warning(ctx, address_n_to_str(path)))
def is_hardened(i: int) -> bool:
@ -278,21 +270,11 @@ def path_is_hardened(address_n: Bip32Path) -> bool:
return all(is_hardened(n) for n in address_n)
def address_n_to_str(address_n: Bip32Path) -> str:
def path_item(i: int) -> str:
if i & HARDENED:
return str(i ^ HARDENED) + "'"
else:
return str(i)
return "m/" + "/".join([path_item(i) for i in address_n])
def break_address_n_to_lines(address_n: Bip32Path) -> List[str]:
lines = []
path_str = address_n_to_str(address_n)
per_line = const(17)
per_line = MONO_CHARS_PER_LINE
while len(path_str) > per_line:
i = path_str[:per_line].rfind("/")
lines.append(path_str[:i])

View File

@ -5,9 +5,8 @@ from trezor.crypto import bip39, slip39
from trezor.messages import BackupType
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.components.tt.text import Text
from trezor.ui.layouts import confirm_action, require
from apps.common.confirm import require_confirm
from apps.management import backup_types
@ -70,7 +69,12 @@ def _validate(msg) -> int:
async def _warn(ctx: wire.Context):
text = Text("Loading seed")
text.bold("Loading private seed", "is not recommended.")
text.normal("Continue only if you", "know what you are doing!")
await require_confirm(ctx, text)
await require(
confirm_action(
ctx,
"warn_loading_seed",
"Loading seed",
"Loading private seed\nis not recommended.",
"Continue only if you\nknow what you are doing!",
)
)

View File

@ -7,6 +7,8 @@ from trezor.messages.EntropyAck import EntropyAck
from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.layouts import confirm_backup, confirm_reset_device, require
from trezor.ui.loader import LoadingAnimation
from .. import backup_types
from ..change_pin import request_pin_confirm
@ -26,7 +28,14 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
_validate_reset_device(msg)
# make sure user knows they're setting up a new wallet
await layout.show_reset_device_warning(ctx, msg.backup_type)
if msg.backup_type == BackupType.Slip39_Basic:
prompt = "Create a new wallet\nwith Shamir Backup?"
elif msg.backup_type == BackupType.Slip39_Advanced:
prompt = "Create a new wallet\nwith Super Shamir?"
else:
prompt = "Do you want to create\na new wallet?"
await require(confirm_reset_device(ctx, prompt))
await LoadingAnimation()
# wipe storage to make sure the device is in a clear state
storage.reset()
@ -68,7 +77,7 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
# If doing backup, ask the user to confirm.
if perform_backup:
perform_backup = await layout.confirm_backup(ctx)
perform_backup = await confirm_backup(ctx)
# generate and display backup information for the master secret
if perform_backup:

View File

@ -9,7 +9,6 @@ from trezor.ui.components.tt.info import InfoConfirm
from trezor.ui.components.tt.num_input import NumInput
from trezor.ui.components.tt.scroll import Paginated
from trezor.ui.components.tt.text import Text
from trezor.ui.loader import LoadingAnimation
from apps.common.confirm import confirm, require_confirm, require_hold_to_confirm
from apps.common.layout import show_success
@ -30,47 +29,6 @@ async def show_internal_entropy(ctx, entropy: bytes):
await require_confirm(ctx, text, ButtonRequestType.ResetDevice)
async def confirm_backup(ctx):
# First prompt
text = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False)
text.bold("New wallet created")
text.br()
text.bold("successfully!")
text.br()
text.br_half()
text.normal("You should back up your")
text.br()
text.normal("new wallet right now.")
if await confirm(
ctx,
text,
ButtonRequestType.ResetDevice,
cancel="Skip",
confirm="Back up",
major_confirm=True,
):
return True
# If the user selects Skip, ask again
text = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False)
text.bold("Are you sure you want")
text.br()
text.bold("to skip the backup?")
text.br()
text.br_half()
text.normal("You can back up your")
text.br()
text.normal("Trezor once, at any time.")
return await confirm(
ctx,
text,
ButtonRequestType.ResetDevice,
cancel="Skip",
confirm="Back up",
major_confirm=True,
)
async def _show_share_words(ctx, share_words, share_index=None, group_index=None):
first, chunks, last = _split_share_into_pages(share_words)
@ -643,27 +601,3 @@ class MnemonicWordSelect(ui.Layout):
def create_tasks(self) -> Tuple[loop.Task, ...]:
return super().create_tasks() + (debug.input_signal(),)
async def show_reset_device_warning(ctx, backup_type: BackupType = BackupType.Bip39):
text = Text("Create new wallet", ui.ICON_RESET, new_lines=False)
if backup_type == BackupType.Slip39_Basic:
text.bold("Create a new wallet")
text.br()
text.bold("with Shamir Backup?")
elif backup_type == BackupType.Slip39_Advanced:
text.bold("Create a new wallet")
text.br()
text.bold("with Super Shamir?")
else:
text.bold("Do you want to create")
text.br()
text.bold("a new wallet?")
text.br()
text.br_half()
text.normal("By continuing you agree")
text.br()
text.normal("to")
text.bold("https://trezor.io/tos")
await require_confirm(ctx, text, ButtonRequestType.ResetDevice, major_confirm=True)
await LoadingAnimation()

View File

@ -1,27 +1,12 @@
import storage
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.messages.Success import Success
from trezor.ui.components.tt.button import ButtonCancel
from trezor.ui.components.tt.text import Text
from trezor.ui.loader import LoaderDanger
from apps.common.confirm import require_hold_to_confirm
from trezor.ui.layouts import confirm_wipe, require
from .apply_settings import reload_settings_from_storage
async def wipe_device(ctx, msg):
text = Text("Wipe device", ui.ICON_WIPE, ui.RED)
text.normal("Do you really want to", "wipe the device?", "")
text.bold("All data will be lost.")
await require_hold_to_confirm(
ctx,
text,
ButtonRequestType.WipeDevice,
confirm_style=ButtonCancel,
loader_style=LoaderDanger,
)
await require(confirm_wipe(ctx))
storage.wipe()
reload_settings_from_storage()

View File

@ -5,3 +5,10 @@ TEXT_LINE_HEIGHT = const(26)
TEXT_LINE_HEIGHT_HALF = const(13)
TEXT_MARGIN_LEFT = const(14)
TEXT_MAX_LINES = const(5)
MONO_CHARS_PER_LINE = const(17)
MONO_HEX_PER_LINE = const(18)
QR_X = const(120)
QR_Y = const(115)
QR_SIZE_THRESHOLD = const(63)

View File

@ -0,0 +1,12 @@
from trezor import utils
from .common import * # noqa: F401,F403
# NOTE: using any import magic probably causes mypy not to check equivalence of
# layout type signatures across models
if utils.MODEL == "1":
from .t1 import * # noqa: F401,F403
elif utils.MODEL == "T":
from .tt import * # noqa: F401,F403
else:
raise ValueError("Unknown Trezor model")

View File

@ -0,0 +1,31 @@
from trezor import log, wire, workflow
from trezor.messages import ButtonRequestType
from trezor.messages.ButtonAck import ButtonAck
from trezor.messages.ButtonRequest import ButtonRequest
from ..components.common.confirm import CONFIRMED
if False:
from typing import Any, Awaitable
from trezor.messages.ButtonRequest import EnumTypeButtonRequestType
LayoutType = Awaitable[Any]
async def require(a: LayoutType) -> None:
result = await a
if result is not CONFIRMED:
raise wire.ActionCancelled
async def interact(
ctx: wire.GenericContext,
layout: LayoutType,
brtype: str,
brcode: EnumTypeButtonRequestType = ButtonRequestType.Other,
) -> Any:
log.debug(__name__, "ButtonRequest.type={}".format(brtype))
workflow.close_others()
await ctx.call(ButtonRequest(code=brcode), ButtonAck)
return await ctx.wait(layout)

View File

View File

@ -0,0 +1,346 @@
from micropython import const
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.container import Container
from trezor.ui.loader import LoaderDanger
from trezor.ui.qr import Qr
from trezor.utils import chunks
from ..components.common import break_path_to_lines
from ..components.common.confirm import CONFIRMED
from ..components.tt.button import ButtonCancel, ButtonDefault
from ..components.tt.confirm import Confirm, HoldToConfirm
from ..components.tt.scroll import Paginated
from ..components.tt.text import Text
from ..constants.tt import (
MONO_CHARS_PER_LINE,
MONO_HEX_PER_LINE,
QR_SIZE_THRESHOLD,
QR_X,
QR_Y,
TEXT_MAX_LINES,
)
from .common import interact
if False:
from typing import Any, Iterator, List, Sequence, Union, Optional
from trezor import wire
from trezor.messages.ButtonRequest import EnumTypeButtonRequestType
from . import LayoutType
__all__ = (
"confirm_action",
"confirm_wipe",
"confirm_reset_device",
"confirm_backup",
"confirm_path_warning",
"show_address",
"confirm_output",
"confirm_hex",
"confirm_total",
"confirm_joint_total",
"confirm_metadata",
"confirm_replacement",
"confirm_modify_fee",
)
def confirm_action(
ctx: wire.GenericContext,
br_type: str,
title: str,
action: str,
description: str = None,
verb: Union[str, bytes] = Confirm.DEFAULT_CONFIRM,
icon: str = None,
br_code: EnumTypeButtonRequestType = ButtonRequestType.Other,
**kwargs: Any,
) -> LayoutType:
text = Text(title, icon if icon is not None else ui.ICON_DEFAULT, new_lines=False)
text.bold(action)
text.br()
if description:
text.normal(description)
return interact(ctx, Confirm(text, confirm=verb), br_type, br_code)
def confirm_wipe(ctx: wire.GenericContext) -> LayoutType:
text = Text("Wipe device", ui.ICON_WIPE, ui.RED)
text.normal("Do you really want to", "wipe the device?", "")
text.bold("All data will be lost.")
return interact(
ctx,
HoldToConfirm(text, confirm_style=ButtonCancel, loader_style=LoaderDanger),
"wipe_device",
ButtonRequestType.WipeDevice,
)
def confirm_reset_device(ctx: wire.GenericContext, prompt: str) -> LayoutType:
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")
return interact(
ctx,
Confirm(text, major_confirm=True),
"setup_device",
ButtonRequestType.ResetDevice,
)
async def confirm_backup(ctx: wire.GenericContext) -> bool:
text1 = Text("Success", ui.ICON_CONFIRM, ui.GREEN)
text1.bold("New wallet created", "successfully!")
text1.br_half()
text1.normal("You should back up your", "new wallet right now.")
text2 = Text("Warning", ui.ICON_WRONG, ui.RED)
text2.bold("Are you sure you want", "to skip the backup?")
text2.br_half()
text2.normal("You can back up your", "Trezor once, at any time.")
if (
await interact(
ctx,
Confirm(text1, cancel="Skip", confirm="Back up", major_confirm=True),
"backup_device",
ButtonRequestType.ResetDevice,
)
is CONFIRMED
):
return True
confirmed = (
await interact(
ctx,
Confirm(text2, cancel="Skip", confirm="Back up", major_confirm=True),
"backup_device",
ButtonRequestType.ResetDevice,
)
) is CONFIRMED
return confirmed
def confirm_path_warning(ctx: wire.GenericContext, path: str) -> LayoutType:
text = Text("Confirm path", ui.ICON_WRONG, ui.RED)
text.normal("Path")
text.mono(*break_path_to_lines(path, MONO_CHARS_PER_LINE))
text.normal("is unknown.", "Are you sure?")
return interact(
ctx,
Confirm(text),
"path_warning",
ButtonRequestType.UnknownDerivationPath,
)
def _show_qr(
address: str,
desc: str,
cancel: str = "Address",
) -> Confirm:
QR_COEF = const(4) if len(address) < QR_SIZE_THRESHOLD else const(3)
qr = Qr(address, QR_X, QR_Y, QR_COEF)
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
return Confirm(Container(qr, text), cancel=cancel, cancel_style=ButtonDefault)
def _split_address(address: str) -> Iterator[str]:
return chunks(address, MONO_CHARS_PER_LINE)
def _hex_lines(hex_data: str, lines: int = TEXT_MAX_LINES) -> Iterator[str]:
if len(hex_data) >= MONO_HEX_PER_LINE * lines:
hex_data = hex_data[: (MONO_HEX_PER_LINE * lines - 3)] + "..."
return chunks(hex_data, MONO_HEX_PER_LINE)
def _show_address(
address: str,
desc: str,
network: str = None,
) -> Confirm:
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
if network is not None:
text.normal("%s network" % network)
text.mono(*_split_address(address))
return Confirm(text, cancel="QR", cancel_style=ButtonDefault)
def _show_xpub(xpub: str, desc: str, cancel: str) -> Paginated:
pages: List[ui.Component] = []
for lines in chunks(list(chunks(xpub, 16)), 5):
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
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_address(
ctx: wire.GenericContext,
address: str,
address_qr: str = None,
desc: str = "Confirm address",
network: str = None,
multisig_index: int = None,
xpubs: Sequence[str] = [],
) -> None:
is_multisig = len(xpubs) > 0
while True:
if (
await interact(
ctx,
_show_address(address, desc, network),
"show_address",
ButtonRequestType.Address,
)
is CONFIRMED
):
break
if (
await interact(
ctx,
_show_qr(
address if address_qr is None else address_qr,
desc,
cancel="XPUBs" if is_multisig else "Address",
),
"show_qr",
ButtonRequestType.Address,
)
is CONFIRMED
):
break
if is_multisig:
for i, xpub in enumerate(xpubs):
cancel = "Next" if i < len(xpubs) - 1 else "Address"
desc = "XPUB #%d" % (i + 1)
desc += " (yours)" if i == multisig_index else " (cosigner)"
if (
await interact(
ctx,
_show_xpub(xpub, desc=desc, cancel=cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
is CONFIRMED
):
return
def confirm_output(
ctx: wire.GenericContext,
address: str,
amount: str,
) -> LayoutType:
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.normal(amount + " to")
text.mono(*_split_address(address))
return interact(
ctx, Confirm(text), "confirm_output", ButtonRequestType.ConfirmOutput
)
def confirm_hex(
ctx: wire.GenericContext,
br_type: str,
title: str,
data: str,
br_code: EnumTypeButtonRequestType = ButtonRequestType.Other,
) -> LayoutType:
text = Text(title, ui.ICON_SEND, ui.GREEN)
text.mono(*_hex_lines(data))
return interact(ctx, Confirm(text), br_type, br_code)
def confirm_total(
ctx: wire.GenericContext, total_amount: str, fee_amount: str
) -> LayoutType:
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal("Total amount:")
text.bold(total_amount)
text.normal("including fee:")
text.bold(fee_amount)
return interact(ctx, HoldToConfirm(text), "confirm_total", ButtonRequestType.SignTx)
def confirm_joint_total(
ctx: wire.GenericContext, spending_amount: str, total_amount: str
) -> LayoutType:
text = Text("Joint transaction", ui.ICON_SEND, ui.GREEN)
text.normal("You are contributing:")
text.bold(spending_amount)
text.normal("to the total amount:")
text.bold(total_amount)
return interact(
ctx, HoldToConfirm(text), "confirm_joint_total", ButtonRequestType.SignTx
)
def confirm_metadata(
ctx: wire.GenericContext,
br_type: str,
title: str,
content: str,
param: Optional[str] = None,
br_code: EnumTypeButtonRequestType = ButtonRequestType.SignTx,
) -> LayoutType:
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
text.format_parametrized(content, param if param is not None else "")
text.br()
text.normal("Continue?")
return interact(ctx, Confirm(text), br_type, br_code)
def confirm_replacement(
ctx: wire.GenericContext, description: str, txid: str
) -> LayoutType:
text = Text(description, ui.ICON_SEND, ui.GREEN)
text.normal("Confirm transaction ID:")
text.mono(*_hex_lines(txid, TEXT_MAX_LINES - 1))
return interact(ctx, Confirm(text), "confirm_replacement", ButtonRequestType.SignTx)
def confirm_modify_fee(
ctx: wire.GenericContext,
sign: int,
user_fee_change: str,
total_fee_new: str,
) -> LayoutType:
text = Text("Fee modification", ui.ICON_SEND, ui.GREEN)
if sign == 0:
text.normal("Your fee did not change.")
else:
if sign < 0:
text.normal("Decrease your fee by:")
else:
text.normal("Increase your fee by:")
text.bold(user_fee_change)
text.br_half()
text.normal("Transaction fee:")
text.bold(total_fee_new)
return interact(ctx, HoldToConfirm(text), "modify_fee", ButtonRequestType.SignTx)

View File

@ -184,7 +184,7 @@
"test_msg_ethereum_signtx_eip155.py::test_chain_ids[609112567-60-sig6]": "c8e01d20eccadcca4f05e4e8351c3bfc38d0fdbe4a61f63dfd74e065faea86e7",
"test_msg_ethereum_signtx_eip155.py::test_chain_ids[61-61-sig3]": "cd5f04cc7b055503e83f0538709a7ac577445c6089ead12f1fc3a3c45ad96419",
"test_msg_ethereum_signtx_eip155.py::test_with_data": "33e97436953f55bc61c5f54bd9702b1ba962c0e716d20ddbe9827be3d24ad98d",
"test_msg_ethereum_verifymessage.py-test_verify": "da70ac961dc8ff548943498f42ad9436d3e2591f42793e37c8ec4cda9ea7835e",
"test_msg_ethereum_verifymessage.py-test_verify": "c1f94d9a7810458643cd441bb9a0844e1fbc519683dd6a5725a9285b247c7648",
"test_msg_ethereum_verifymessage.py-test_verify_invalid": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_getaddress.py-test_bch": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_getaddress.py-test_bch_multisig": "f620ed42c682dd55ab7a1b4ac53686c03dd51966ad4bdd018bd24a3305b92148",
@ -284,7 +284,7 @@
"test_msg_lisk_signtx.py-test_lisk_sign_tx_send": "07e5a905588b9e2518db7554c76c548c5a1267849b30cf911259477938705ff8",
"test_msg_lisk_signtx.py-test_lisk_sign_tx_send_with_data": "ea969f90b6e4b840bb8728a9a99e5d07f59f478dba6b144218148d9db83f7a49",
"test_msg_lisk_verifymessage.py-test_verify": "492d75b6126690f5ecb17132cd1dafcf0da9bddbb8fecd3c32e7654f346039e2",
"test_msg_lisk_verifymessage.py-test_verify_long": "8158d14f6ceabb8865f3c5631360673c4c2226e563d7e18342f18f8af2b7c8a1",
"test_msg_lisk_verifymessage.py-test_verify_long": "89d00ad07a5951e47484879b9bdb7998f3364b755aafb3e7e783c28e0521a8e7",
"test_msg_loaddevice.py-test_load_device_1": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
"test_msg_loaddevice.py-test_load_device_2": "dc13c8486d8a59c5062e19139d8b3cea4ece1a3bc93592be7dc226f83ba54477",
"test_msg_loaddevice.py-test_load_device_slip39_advanced": "1c6db0d592b1d22b3c9fce3ddab8a9fd138f11d83e5d4e64431a02bf4ffed605",
@ -309,7 +309,7 @@
"test_msg_nem_signtx_transfers.py-test_nem_signtx_simple": "d36a67610b16f835b174a053fe60104a03ea5d49fbd612d73f5d8cdb31fce421",
"test_msg_nem_signtx_transfers.py-test_nem_signtx_unknown_mosaic": "1fd9bf33c3c481d8b76fbdddfc3e8d91df6a1a97661a8f8c4b57cd3df41e83f0",
"test_msg_nem_signtx_transfers.py-test_nem_signtx_xem_as_mosaic": "842307e1734fea44aca9e53e2d76e0c6206348c4461f9eb1a36021bed1f681c8",
"test_msg_ping.py::test_ping": "229a51d6e66ea58927167e84f96d5d4d30dd0ebace0258fcb709f841ce0beb9e",
"test_msg_ping.py::test_ping": "949781cf7d772de19cca691adecdca523f5cada8f83b5713af0bc179435995e4",
"test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[label-test]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[language-test]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_recoverydevice_bip39_dryrun.py::test_bad_parameters[passphrase_protection-True]": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
@ -394,10 +394,10 @@
"test_msg_signtx.py-test_incorrect_input_script_type[2]": "ff8306b910f6886638e30736acd025ff7f45dde3c6648de1f6c6922bc6f590c5",
"test_msg_signtx.py-test_incorrect_output_script_type[0]": "ff8306b910f6886638e30736acd025ff7f45dde3c6648de1f6c6922bc6f590c5",
"test_msg_signtx.py-test_incorrect_output_script_type[1]": "ff8306b910f6886638e30736acd025ff7f45dde3c6648de1f6c6922bc6f590c5",
"test_msg_signtx.py-test_lock_time[1-4294967295]": "d805244ea557c3695101a6f79f13045f22bc16d5608744e0321eab7f3a98d8b0",
"test_msg_signtx.py-test_lock_time[1-4294967295]": "da3d6b38ec9264e38f3547427eb748d7b94e0802eb2aee411e0f72bf97620280",
"test_msg_signtx.py-test_lock_time[499999999-4294967294]": "23a154e7b40680161bb099cfc6702d75909c222056867515647123573eef1716",
"test_msg_signtx.py-test_lock_time[500000000-4294967294]": "2ac61446c20785e45223a20ae90660905fedf20d2a383a65fcbc1edd5fe87ad1",
"test_msg_signtx.py-test_lots_of_change": "9e143458b399d187b6a3060fc95b998822f5a7ed67d6915610fd02c0ccab791e",
"test_msg_signtx.py-test_lots_of_change": "0fc26d5d47b2fd07b3423c4092cdeb27c1af2a8095eed375dd1e8db10766135a",
"test_msg_signtx.py-test_lots_of_inputs": "7d02dd952be20b46005af23c8b46a95590529d8e838459e540eca0b058fc32c1",
"test_msg_signtx.py-test_lots_of_outputs": "f9d50f30dbdaeddf1f54e8bf76dce07fa9d40dcb7ef36a908e76123f2151501c",
"test_msg_signtx.py-test_not_enough_funds": "dbaa027aa1f4b08b138a5965245593dab2a662b0f4d88dd28b82a64f88f5d7fe",
@ -463,8 +463,8 @@
"test_msg_signtx_grs.py-test_send_segwit_p2sh_change": "6c352ab975a75a150f7c3415a967fb8635395ff8db0de89ecb9c2011cb519509",
"test_msg_signtx_invalid_path.py-test_invalid_path_fail": "b0f22cba2dbab2cd21c15c002b66ed89b6c728b10daa8d0c0e78abd4164a3912",
"test_msg_signtx_invalid_path.py-test_invalid_path_pass_forkid": "667dcb09b569e5b4e091e6b1ac7e8e057c0c730c931b22f8c0ee64050f3f467b",
"test_msg_signtx_komodo.py-test_one_one_fee_sapling": "14bad8852ee51f6fec12677cced9ffafa0cbae91b4ba94e988a800544072ed21",
"test_msg_signtx_komodo.py-test_one_one_rewards_claim": "751e83d63bf01c6c57047b5e004629d613df75342371cd43a7b4b80a07f4b88d",
"test_msg_signtx_komodo.py-test_one_one_fee_sapling": "5643a961909bbac2ff7cc7df9766836957ba78b2bf35ba101f2ef7df18445cfe",
"test_msg_signtx_komodo.py-test_one_one_rewards_claim": "0cd0d0609522ace94f970ded00f7aebfe0503d2894d516aa9d674b9573779d2d",
"test_msg_signtx_mixed_inputs.py::test_non_segwit_segwit_inputs": "d72acb396bbc3109054919bddc823e8900bb30b6c41c553922beb449af9bb51d",
"test_msg_signtx_mixed_inputs.py::test_non_segwit_segwit_non_segwit_inputs": "50b846945367990f0b6bcad9161cb899b265c9540a2578e0f5c39533a7ec0010",
"test_msg_signtx_mixed_inputs.py::test_segwit_non_segwit_inputs": "d72acb396bbc3109054919bddc823e8900bb30b6c41c553922beb449af9bb51d",
@ -551,17 +551,17 @@
"test_msg_tezos_sign_tx.py-test_tezos_smart_contract_transfer": "2d6b7f18fb79676707e58804414553e95bf3aca8eb15bc513137e81de73441a0",
"test_msg_tezos_sign_tx.py-test_tezos_smart_contract_transfer_to_contract": "88eed16fe60952c069326b1a74b3ee3d65598f42381e51d9a17ed8618a567612",
"test_msg_verifymessage.py-test_message_grs": "6cc8d8e5abffb5e956e6f5bb9ff7c973ec85209012deb27ee0d234301a1c9819",
"test_msg_verifymessage.py-test_message_long": "3cdeddf3c32147a1a7bbf0b6b303d022a6ce6c13f8ed1f480ca95972c95e3467",
"test_msg_verifymessage.py-test_message_long": "e234504e23eab8159e229617c183db6a3eac411bdb49cadd0502361d52c00455",
"test_msg_verifymessage.py-test_message_testnet": "8649c8d0eeaef9bfa0b3d13466bc2bb60f2059b52effd3937ffdf3443287b78a",
"test_msg_verifymessage.py-test_message_verify": "dc98df956c9160bdaa6535c3de2760f86e31e5200c453d1593c9791b97214138",
"test_msg_verifymessage.py-test_message_verify_bcash": "55fcea392b0f4cb3cac16d172edf6024fa0de5ec6d9421fa4eca4ef245fe16ca",
"test_msg_verifymessage.py-test_verify_bitcoind": "f4c1e9be6d5f3ad2aa5fbc23389ea44f185b606b5c7eed4b72506cc2afd8829f",
"test_msg_verifymessage.py-test_verify_utf": "a312ddde284be0b6f251c3474cf7c2a96ac5b7bd3f27da6c2203eb06fdce8655",
"test_msg_verifymessage_segwit.py-test_message_long": "fd89ce23a94862326524b2ac4edaf7153553960a2168e6e148c59734030da305",
"test_msg_verifymessage_segwit.py-test_message_long": "9224423991d092bb9aed5cb91199c939cbd7168696bf2474d4319b0d494abf94",
"test_msg_verifymessage_segwit.py-test_message_testnet": "f602742784969a434a44dee2e7d939af1c5f24beb5f78cca397a5cab85176173",
"test_msg_verifymessage_segwit.py-test_message_verify": "3bb8619103ed01ae6f8f7744e7ef3031f42699c699b0b4866ac8eac707e04428",
"test_msg_verifymessage_segwit.py-test_verify_utf": "a1a17b676a1a0e44c122a815825ef572edb77d83d4ba31ef20b9b2e54f44ccfe",
"test_msg_verifymessage_segwit_native.py-test_message_long": "cc515be104597dffdd0389e3dcf1a7cd42cc2d8c8586d48466d6f269eabff66f",
"test_msg_verifymessage_segwit_native.py-test_message_long": "f8075cdfb9d4c6790109a74d698150bd609ce2684ab8a98a81b9c4c7098f02aa",
"test_msg_verifymessage_segwit_native.py-test_message_testnet": "c4dc0ee5a473449455a58675d6d1a72ddc131de012e69f9c95df9a314a4650fc",
"test_msg_verifymessage_segwit_native.py-test_message_verify": "3aeca0b02254b83988008b5129812a749f320add09146d189fa294f2b5c80c34",
"test_msg_verifymessage_segwit_native.py-test_verify_utf": "62d12291ee0f0d4639d861ea61d55c9944c37aad24bd70dd35877e9d12a2b731",