core: revamp the UI subsystem

pull/190/head
Jan Pochyla 5 years ago
parent 2f4c123466
commit 36534325f0

@ -1,13 +1,12 @@
from micropython import const
from trezor import ui
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks, format_amount
from apps.common.confirm import confirm, hold_to_confirm
def format_coin_amount(amount):
return "%s %s" % (format_amount(amount, 6), "ADA")
@ -16,107 +15,34 @@ def format_coin_amount(amount):
async def confirm_sending(ctx, amount, to):
to_lines = list(chunks(to, 17))
t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
t1.normal("Confirm sending:")
t1.bold(format_coin_amount(amount))
t1.normal("to:")
t1.bold(to_lines[0])
pages = [t1]
LINES_PER_PAGE = 4
PER_PAGE = const(4)
pages = [t1]
if len(to_lines) > 1:
to_pages = list(chunks(to_lines[1:], LINES_PER_PAGE))
to_pages = list(chunks(to_lines[1:], PER_PAGE))
for page in to_pages:
t = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
t = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
for line in page:
t.bold(line)
pages.append(t)
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck)
paginator = paginate(create_renderer(ConfirmDialog), len(pages), const(0), pages)
return await ctx.wait(paginator) == CONFIRMED
return await confirm(ctx, Paginated(pages))
async def confirm_transaction(ctx, amount, fee, network_name):
t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
t1.normal("Total amount:")
t1.bold(format_coin_amount(amount))
t1.normal("including fee:")
t1.bold(format_coin_amount(fee))
t2 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
t2.normal("Network:")
t2.bold(network_name)
pages = [t1, t2]
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck)
paginator = paginate(
create_renderer(HoldToConfirmDialog), len(pages), const(0), pages
)
return await ctx.wait(paginator) == CONFIRMED
def create_renderer(confirmation_wrapper):
@ui.layout
async def page_renderer(page: int, page_count: int, pages: list):
# for some reason page index can be equal to page count
if page >= page_count:
page = page_count - 1
pages[page].taint()
content = Scrollpage(pages[page], page, page_count)
if page + 1 >= page_count:
return await confirmation_wrapper(content)
else:
content.render()
await animate_swipe()
return page_renderer
async def confirm_with_pagination(
ctx, content, title: str, icon=ui.ICON_RESET, icon_color=ui.ORANGE
):
first_page = const(0)
lines_per_page = const(4)
if isinstance(content, (list, tuple)):
lines = content
else:
lines = list(chunks(content, 17))
pages = list(chunks(lines, lines_per_page))
await ctx.call(ButtonRequest(code=ButtonRequestType.Address), MessageType.ButtonAck)
paginator = paginate(
show_text_page, len(pages), first_page, pages, title, icon, icon_color
)
return await ctx.wait(paginator) == CONFIRMED
@ui.layout
async def show_text_page(
page: int,
page_count: int,
pages: list,
title: str,
icon=ui.ICON_RESET,
icon_color=ui.ORANGE,
):
if page_count == 1:
page = 0
lines = pages[page]
content = Text(title, icon, icon_color=icon_color)
content.mono(*lines)
content = Scrollpage(content, page, page_count)
if page + 1 >= page_count:
return await ConfirmDialog(content)
else:
content.render()
await animate_swipe()
return await hold_to_confirm(ctx, Paginated([t1, t2]))

@ -1,29 +1,59 @@
from trezor import ui, wire
from trezor import wire
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog
from trezor.ui.confirm import CONFIRMED, Confirm, HoldToConfirm
if __debug__:
from apps.debug import confirm_signal
@ui.layout
async def confirm(ctx, content, code=None, *args, **kwargs):
if code is None:
code = ButtonRequestType.Other
async def confirm(
ctx,
content,
code=ButtonRequestType.Other,
confirm=Confirm.DEFAULT_CONFIRM,
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
cancel=Confirm.DEFAULT_CANCEL,
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
):
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
dialog = ConfirmDialog(content, *args, **kwargs)
if content.__class__.__name__ == "Paginated":
content.pages[-1] = Confirm(
content.pages[-1], confirm, confirm_style, cancel, cancel_style
)
dialog = content
else:
dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style)
return await ctx.wait(dialog) == CONFIRMED
if __debug__:
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
else:
return await ctx.wait(dialog) is CONFIRMED
@ui.layout
async def hold_to_confirm(ctx, content, code=None, *args, **kwargs):
if code is None:
code = ButtonRequestType.Other
async def hold_to_confirm(
ctx,
content,
code=ButtonRequestType.Other,
confirm=HoldToConfirm.DEFAULT_CONFIRM,
confirm_style=HoldToConfirm.DEFAULT_CONFIRM_STYLE,
loader_style=HoldToConfirm.DEFAULT_LOADER_STYLE,
):
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
dialog = HoldToConfirmDialog(content, "Hold to confirm", *args, **kwargs)
if content.__class__.__name__ == "Paginated":
content.pages[-1] = HoldToConfirm(
content.pages[-1], confirm, confirm_style, loader_style
)
dialog = content
else:
dialog = HoldToConfirm(content, confirm, confirm_style, loader_style)
return await ctx.wait(dialog) == CONFIRMED
if __debug__:
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
else:
return await ctx.wait(dialog) is CONFIRMED
async def require_confirm(*args, **kwargs):

@ -3,6 +3,7 @@ from ubinascii import hexlify
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.button import ButtonDefault
from trezor.ui.container import Container
from trezor.ui.qr import Qr
from trezor.ui.text import Text
@ -12,42 +13,45 @@ from apps.common import HARDENED
from apps.common.confirm import confirm, require_confirm
async def show_address(ctx, address: str, desc: str = None, network: str = None):
text = Text(
desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN
)
if network:
async def show_address(
ctx, address: str, desc: str = "Confirm address", network: str = None
):
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
if network is not None:
text.normal("%s network" % network)
text.mono(*split_address(address))
return await confirm(
ctx, text, code=ButtonRequestType.Address, cancel="QR", cancel_style=ui.BTN_KEY
ctx,
text,
code=ButtonRequestType.Address,
cancel="QR",
cancel_style=ButtonDefault,
)
async def show_qr(ctx, address: str, desc: str = None):
qr_x = const(120)
qr_y = const(115)
qr_coef = const(4)
qr = Qr(address, (qr_x, qr_y), qr_coef)
text = Text(
desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN
)
async def show_qr(ctx, address: str, desc: str = "Confirm address"):
QR_X = const(120)
QR_Y = const(115)
QR_COEF = const(4)
qr = Qr(address, QR_X, QR_Y, QR_COEF)
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
content = Container(qr, text)
return await confirm(
ctx,
content,
code=ButtonRequestType.Address,
cancel="Address",
cancel_style=ui.BTN_KEY,
cancel_style=ButtonDefault,
)
async def show_pubkey(ctx, pubkey: bytes):
lines = chunks(hexlify(pubkey).decode(), 18)
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
text.mono(*lines)
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
return await require_confirm(ctx, text, ButtonRequestType.PublicKey)
def split_address(address: str):

@ -1,4 +1,4 @@
from trezor import ui
from trezor import ui, workflow
from trezor.crypto import bip39
from apps.common import storage
@ -18,8 +18,11 @@ def get_seed(passphrase: str = "", progress_bar=True):
module = bip39
if progress_bar:
_start_progress()
return module.seed(secret.decode(), passphrase, _render_progress)
return module.seed(secret.decode(), passphrase)
result = module.seed(secret.decode(), passphrase, _render_progress)
_stop_progress()
else:
result = module.seed(secret.decode(), passphrase)
return result
def process(mnemonics: list, mnemonic_type: int):
@ -36,14 +39,18 @@ def restore() -> str:
def _start_progress():
ui.backlight_slide_sync(ui.BACKLIGHT_DIM)
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
def _render_progress(progress: int, total: int):
p = 1000 * progress // total
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()
def _stop_progress():
workflow.restartdefault()

@ -15,14 +15,12 @@ async def validate_path(ctx, validate_func, keychain, path, curve, **kwargs):
async def show_path_warning(ctx, path: list):
text = Text("Confirm path", ui.ICON_WRONG, icon_color=ui.RED)
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?")
return await require_confirm(
ctx, text, code=ButtonRequestType.UnknownDerivationPath
)
return await require_confirm(ctx, text, ButtonRequestType.UnknownDerivationPath)
def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool:
@ -50,7 +48,8 @@ def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool:
def is_hardened(i: int) -> bool:
if i & HARDENED:
return True
return False
else:
return False
def break_address_n_to_lines(address_n: list) -> list:

@ -5,79 +5,74 @@ from trezor.messages import ButtonRequestType, MessageType, PassphraseSourceType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.PassphraseRequest import PassphraseRequest
from trezor.messages.PassphraseStateRequest import PassphraseStateRequest
from trezor.ui.entry_select import DEVICE, EntrySelector
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard, PassphraseSource
from trezor.ui.text import Text
from apps.common import storage
from apps.common.cache import get_state
from apps.common import cache, storage
if __debug__:
from apps.debug import input_signal
_MAX_PASSPHRASE_LEN = const(50)
@ui.layout
async def request_passphrase_entry(ctx):
text = Text("Enter passphrase", ui.ICON_CONFIG)
text.normal("Where to enter your", "passphrase?")
text.render()
async def protect_by_passphrase(ctx) -> str:
if storage.has_passphrase():
return await request_passphrase(ctx)
else:
return ""
ack = await ctx.call(
ButtonRequest(code=ButtonRequestType.PassphraseType),
MessageType.ButtonAck,
MessageType.Cancel,
async def request_passphrase(ctx) -> str:
source = storage.get_passphrase_source()
if source == PassphraseSourceType.ASK:
source = await request_passphrase_source(ctx)
passphrase = await request_passphrase_ack(
ctx, source == PassphraseSourceType.DEVICE
)
if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel:
raise wire.ActionCancelled("Passphrase cancelled")
if len(passphrase) > _MAX_PASSPHRASE_LEN:
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
return passphrase
async def request_passphrase_source(ctx) -> int:
req = ButtonRequest(code=ButtonRequestType.PassphraseType)
await ctx.call(req, MessageType.ButtonAck)
text = Text("Enter passphrase", ui.ICON_CONFIG)
text.normal("Where to enter your", "passphrase?")
source = PassphraseSource(text)
selector = EntrySelector(text)
return await ctx.wait(selector)
return await ctx.wait(source)
@ui.layout
async def request_passphrase_ack(ctx, on_device):
async def request_passphrase_ack(ctx, on_device: bool) -> str:
if not on_device:
text = Text("Passphrase entry", ui.ICON_CONFIG)
text.normal("Please, type passphrase", "on connected host.")
text.render()
req = PassphraseRequest(on_device=on_device)
ack = await ctx.call(req, MessageType.PassphraseAck, MessageType.Cancel)
if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel:
raise wire.ActionCancelled("Passphrase cancelled")
ack = await ctx.call(req, MessageType.PassphraseAck)
if on_device:
if ack.passphrase is not None:
raise wire.ProcessError("Passphrase provided when it should not be")
keyboard = PassphraseKeyboard("Enter passphrase")
passphrase = await ctx.wait(keyboard)
if passphrase == CANCELLED:
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
if __debug__:
passphrase = await ctx.wait(keyboard, input_signal)
else:
passphrase = await ctx.wait(keyboard)
if passphrase is CANCELLED:
raise wire.ActionCancelled("Passphrase cancelled")
else:
if ack.passphrase is None:
raise wire.ProcessError("Passphrase not provided")
passphrase = ack.passphrase
req = PassphraseStateRequest(
state=get_state(prev_state=ack.state, passphrase=passphrase)
)
state = cache.get_state(prev_state=ack.state, passphrase=passphrase)
req = PassphraseStateRequest(state=state)
ack = await ctx.call(req, MessageType.PassphraseStateAck, MessageType.Cancel)
return passphrase
async def request_passphrase(ctx):
if storage.get_passphrase_source() == PassphraseSourceType.ASK:
on_device = await request_passphrase_entry(ctx) == DEVICE
else:
on_device = storage.get_passphrase_source() == PassphraseSourceType.DEVICE
passphrase = await request_passphrase_ack(ctx, on_device)
if len(passphrase) > _MAX_PASSPHRASE_LEN:
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
return passphrase
async def protect_by_passphrase(ctx):
if storage.has_passphrase():
return await request_passphrase(ctx)
else:
return ""

@ -1,6 +1,5 @@
from trezor import loop, res, ui
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
from trezor.ui.pin import PinMatrix
from trezor import loop
from trezor.ui.pin import CANCELLED, PinDialog
if __debug__:
from apps.debug import input_signal
@ -10,71 +9,25 @@ class PinCancelled(Exception):
pass
@ui.layout
async def request_pin(
label=None, attempts_remaining=None, cancellable: bool = True
prompt: str = "Enter your PIN",
attempts_remaining: int = None,
allow_cancel: bool = True,
) -> str:
def onchange():
c = dialog.cancel
if matrix.pin:
back = res.load(ui.ICON_BACK)
if c.content is not back:
c.normal_style = ui.BTN_CLEAR["normal"]
c.content = back
c.enable()
c.taint()
else:
lock = res.load(ui.ICON_LOCK)
if not cancellable and c.content:
c.content = ""
c.disable()
c.taint()
elif c.content is not lock:
c.normal_style = ui.BTN_CANCEL["normal"]
c.content = lock
c.enable()
c.taint()
c.render()
c = dialog.confirm
if matrix.pin:
if not c.is_enabled():
c.enable()
c.taint()
else:
if c.is_enabled():
c.disable()
c.taint()
c.render()
if attempts_remaining is None:
subprompt = None
elif attempts_remaining == 1:
subprompt = "This is your last attempt"
else:
subprompt = "%s attempts remaining" % attempts_remaining
if label is None:
label = "Enter your PIN"
sublabel = None
if attempts_remaining:
if attempts_remaining == 1:
sublabel = "This is your last attempt"
else:
sublabel = "{} attempts remaining".format(attempts_remaining)
matrix = PinMatrix(label, sublabel)
matrix.onchange = onchange
dialog = ConfirmDialog(matrix)
dialog.cancel.area = ui.grid(12)
dialog.confirm.area = ui.grid(14)
matrix.onchange()
dialog = PinDialog(prompt, subprompt, allow_cancel)
while True:
if __debug__:
result = await loop.spawn(dialog, input_signal)
if isinstance(result, str):
return result
else:
result = await dialog
if result == CONFIRMED:
if not matrix.pin:
continue
return matrix.pin
elif matrix.pin: # reset
matrix.change("")
continue
else: # cancel
if result is CANCELLED:
raise PinCancelled()
return result

@ -1,4 +1,4 @@
from trezor import ui, wire
from trezor import wire
from trezor.crypto import bip32
from apps.common import HARDENED, cache, mnemonic, storage
@ -64,30 +64,16 @@ async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain:
raise wire.ProcessError("Device is not initialized")
seed = cache.get_seed()
if seed is None:
seed = await _compute_seed(ctx)
passphrase = cache.get_passphrase()
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
cache.set_passphrase(passphrase)
seed = mnemonic.get_seed(passphrase)
cache.set_seed(seed)
keychain = Keychain(seed, namespaces)
return keychain
def _path_hardened(path: list) -> bool:
# TODO: move to paths.py after #538 is fixed
for i in path:
if not (i & HARDENED):
return False
return True
@ui.layout_no_slide
async def _compute_seed(ctx: wire.Context) -> bytes:
passphrase = cache.get_passphrase()
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
cache.set_passphrase(passphrase)
seed = mnemonic.get_seed(passphrase)
cache.set_seed(seed)
return seed
def derive_node_without_passphrase(
path: list, curve_name: str = "secp256k1"
) -> bip32.HDNode:
@ -102,3 +88,7 @@ def derive_node_without_passphrase(
def remove_ed25519_prefix(pubkey: bytes) -> bytes:
# 0x01 prefix is not part of the actual public key, hence removed
return pubkey[1:]
def _path_hardened(path: list) -> bool:
return all(i & HARDENED for i in path)

@ -1,7 +1,7 @@
from micropython import const
from ubinascii import hexlify
from trezor import ui, wire
from trezor import ui
from trezor.messages import (
ButtonRequestType,
EosActionBuyRam,
@ -17,11 +17,8 @@ from trezor.messages import (
EosActionUnlinkAuth,
EosActionUpdateAuth,
EosActionVoteProducer,
MessageType,
)
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks
@ -38,11 +35,19 @@ _FOUR_FIELDS_PER_PAGE = const(4)
_FIVE_FIELDS_PER_PAGE = const(5)
async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
async def _require_confirm_paginated(ctx, header, fields, per_page):
pages = []
for page in chunks(fields, per_page):
if header == "Arbitrary data":
text = Text(header, ui.ICON_WIPE, ui.RED)
else:
text = Text(header, ui.ICON_CONFIRM, ui.GREEN)
text.mono(*page)
pages.append(text)
await require_confirm(ctx, Paginated(pages), ButtonRequestType.ConfirmOutput)
async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
text = "Buy RAM"
fields = []
fields.append("Payer:")
@ -51,18 +56,10 @@ async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
fields.append(helpers.eos_name_to_string(msg.receiver))
fields.append("Amount:")
fields.append(helpers.eos_asset_to_string(msg.quantity))
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Buy RAM"
fields = []
fields.append("Payer:")
@ -71,18 +68,10 @@ async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes):
fields.append(helpers.eos_name_to_string(msg.receiver))
fields.append("Bytes:")
fields.append(str(msg.bytes))
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_delegate(ctx, msg: EosActionDelegate):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Delegate"
fields = []
fields.append("Sender:")
@ -101,35 +90,20 @@ async def confirm_action_delegate(ctx, msg: EosActionDelegate):
else:
fields.append("Transfer: No")
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_sellram(ctx, msg: EosActionSellRam):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Sell RAM"
fields = []
fields.append("Receiver:")
fields.append(helpers.eos_name_to_string(msg.account))
fields.append("Bytes:")
fields.append(str(msg.bytes))
pages = list(chunks(fields, _TWO_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _TWO_FIELDS_PER_PAGE)
async def confirm_action_undelegate(ctx, msg: EosActionUndelegate):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Undelegate"
fields = []
fields.append("Sender:")
@ -140,11 +114,7 @@ async def confirm_action_undelegate(ctx, msg: EosActionUndelegate):
fields.append(helpers.eos_asset_to_string(msg.cpu_quantity))
fields.append("NET:")
fields.append(helpers.eos_asset_to_string(msg.net_quantity))
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_refund(ctx, msg: EosActionRefund):
@ -166,13 +136,12 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer):
elif msg.producers:
# PRODUCERS
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
producers = list(enumerate(msg.producers))
pages = list(chunks(producers, _FIVE_FIELDS_PER_PAGE))
paginator = paginate(show_voter_page, len(pages), _FIRST_PAGE, pages)
await ctx.wait(paginator)
text = "Vote for producers"
fields = [
"{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer))
for wi, producer in enumerate(msg.producers)
]
await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE)
else:
# Cancel vote
@ -183,10 +152,6 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer):
async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Transfer"
fields = []
fields.append("From:")
@ -200,19 +165,12 @@ async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str):
if msg.memo is not None:
fields.append("Memo:")
fields += split_data(msg.memo[:512])
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
fields.extend(split_data(msg.memo[:512]))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Update Auth"
fields = []
fields.append("Account:")
@ -221,12 +179,8 @@ async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth):
fields.append(helpers.eos_name_to_string(msg.permission))
fields.append("Parent:")
fields.append(helpers.eos_name_to_string(msg.parent))
fields += authorization_fields(msg.auth)
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
fields.extend(authorization_fields(msg.auth))
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth):
@ -239,10 +193,6 @@ async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth):
async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Link Auth"
fields = []
fields.append("Account:")
@ -253,18 +203,10 @@ async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth):
fields.append(helpers.eos_name_to_string(msg.type))
fields.append("Requirement:")
fields.append(helpers.eos_name_to_string(msg.requirement))
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Unlink Auth"
fields = []
fields.append("Account:")
@ -273,86 +215,31 @@ async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth):
fields.append(helpers.eos_name_to_string(msg.code))
fields.append("Type:")
fields.append(helpers.eos_name_to_string(msg.type))
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_newaccount(ctx, msg: EosActionNewAccount):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "New Account"
fields = []
fields.append("Creator:")
fields.append(helpers.eos_name_to_string(msg.creator))
fields.append("Name:")
fields.append(helpers.eos_name_to_string(msg.name))
fields += authorization_fields(msg.owner)
fields += authorization_fields(msg.active)
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
fields.extend(authorization_fields(msg.owner))
fields.extend(authorization_fields(msg.active))
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
async def confirm_action_unknown(ctx, action, checksum):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
)
text = "Arbitrary data"
fields = []
fields.append("Contract:")
fields.append(helpers.eos_name_to_string(action.account))
fields.append("Action Name:")
fields.append(helpers.eos_name_to_string(action.name))
fields.append("Checksum: ")
fields += split_data(hexlify(checksum).decode("ascii"))
pages = list(chunks(fields, _FIVE_FIELDS_PER_PAGE))
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
await ctx.wait(paginator)
@ui.layout
async def show_lines_page(page: int, page_count: int, pages: list, header: str):
if header == "Arbitrary data":
text = Text(header, ui.ICON_WIPE, icon_color=ui.RED)
else:
text = Text(header, ui.ICON_CONFIRM, icon_color=ui.GREEN)
text.mono(*pages[page])
content = Scrollpage(text, page, page_count)
if page + 1 == page_count:
if await ConfirmDialog(content) != CONFIRMED:
raise wire.ActionCancelled("Action cancelled")
else:
content.render()
await animate_swipe()
@ui.layout
async def show_voter_page(page: int, page_count: int, pages: list):
lines = [
"{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer))
for wi, producer in pages[page]
]
text = Text("Vote for producers", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text.mono(*lines)
content = Scrollpage(text, page, page_count)
if page + 1 == page_count:
if await ConfirmDialog(content) != CONFIRMED:
raise wire.ActionCancelled("Action cancelled")
else:
content.render()
await animate_swipe()
fields.extend(split_data(hexlify(checksum).decode("ascii")))
await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE)
def authorization_fields(auth):

@ -6,14 +6,14 @@ from apps.common.confirm import require_confirm
async def require_get_public_key(ctx, public_key):
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
text.normal(public_key)
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
return await require_confirm(ctx, text, ButtonRequestType.PublicKey)
async def require_sign_tx(ctx, num_actions):
text = Text("Sign transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Sign transaction", ui.ICON_SEND, ui.GREEN)
text.normal("You are about")
text.normal("to sign {}".format(num_actions))
text.normal("action(s).")
return await require_confirm(ctx, text, code=ButtonRequestType.SignTx)
return await require_confirm(ctx, text, ButtonRequestType.SignTx)

@ -16,7 +16,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type
to_str = address_from_bytes(to_bytes, networks.by_chain_id(chain_id))
else:
to_str = "new contract?"
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.bold(format_ethereum_amount(value, token, chain_id, tx_type))
text.normal(ui.GREY, "to", ui.FG)
for to_line in split_address(to_str):
@ -29,9 +29,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type
async def require_confirm_fee(
ctx, spending, gas_price, gas_limit, chain_id, token=None, tx_type=None
):
text = Text(
"Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False
)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.bold(format_ethereum_amount(spending, token, chain_id, tx_type))
text.normal(ui.GREY, "Gas price:", ui.FG)
text.bold(format_ethereum_amount(gas_price, None, chain_id, tx_type))
@ -48,7 +46,7 @@ async def require_confirm_data(ctx, data, data_total):
data_str = hexlify(data[:36]).decode()
if data_total > 36:
data_str = data_str[:-2] + ".."
text = Text("Confirm data", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm data", ui.ICON_SEND, ui.GREEN)
text.bold("Size: %d bytes" % data_total)
text.mono(*split_data(data_str))
# we use SignTx, not ConfirmOutput, for compatibility with T1

@ -5,9 +5,9 @@ from apps.common import storage
async def homescreen():
# render homescreen in dimmed mode and fade back in
await ui.backlight_slide(ui.BACKLIGHT_DIM)
ui.backlight_fade(ui.BACKLIGHT_DIM)
display_homescreen()
await ui.backlight_slide(ui.BACKLIGHT_NORMAL)
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
# loop forever, never return
touch = loop.wait(io.TOUCH)

@ -10,7 +10,7 @@ from apps.common.layout import show_pubkey, split_address
async def require_confirm_tx(ctx, to, value):
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.bold(format_coin_amount(value))
text.normal("to")
text.mono(*split_address(to))
@ -18,7 +18,7 @@ async def require_confirm_tx(ctx, to, value):
async def require_confirm_delegate_registration(ctx, delegate_name):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal("Do you really want to")
text.normal("register a delegate?")
text.bold(*chunks(delegate_name, 20))
@ -26,7 +26,7 @@ async def require_confirm_delegate_registration(ctx, delegate_name):
async def require_confirm_vote_tx(ctx, votes):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal(*get_vote_tx_text(votes))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
@ -36,7 +36,7 @@ async def require_confirm_public_key(ctx, public_key):
async def require_confirm_multisig(ctx, multisignature):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal("Keys group length: %s" % len(multisignature.keys_group))
text.normal("Life time: %s" % multisignature.life_time)
text.normal("Min: %s" % multisignature.min)
@ -44,7 +44,7 @@ async def require_confirm_multisig(ctx, multisignature):
async def require_confirm_fee(ctx, value, fee):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.bold(format_coin_amount(value))
text.normal("fee:")
text.bold(format_coin_amount(fee))

@ -51,14 +51,14 @@ async def apply_settings(ctx, msg):
async def require_confirm_change_homescreen(ctx):
text = Text("Change homescreen", ui.ICON_CONFIG)
text.normal("Do you really want to", "change homescreen?")
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
async def require_confirm_change_label(ctx, label):
text = Text("Change label", ui.ICON_CONFIG)
text.normal("Do you really want to", "change label to")
text.bold("%s?" % label)
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
async def require_confirm_change_passphrase(ctx, use):
@ -66,7 +66,7 @@ async def require_confirm_change_passphrase(ctx, use):
text.normal("Do you really want to")
text.normal("enable passphrase" if use else "disable passphrase")
text.normal("encryption?")
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
async def require_confirm_change_passphrase_source(ctx, source):
@ -79,7 +79,7 @@ async def require_confirm_change_passphrase_source(ctx, source):
text = Text("Passphrase source", ui.ICON_CONFIG)
text.normal("Do you really want to", "change the passphrase", "source to")
text.bold("ALWAYS %s?" % desc)
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
async def require_confirm_change_display_rotation(ctx, rotation):
@ -95,4 +95,4 @@ async def require_confirm_change_display_rotation(ctx, rotation):
text.normal("Do you really want to", "change display rotation")
text.normal("to")
text.bold("%s?" % label)
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)

@ -4,8 +4,8 @@ from trezor.messages.Success import Success
from apps.common import mnemonic, storage
from apps.management.reset_device import (
check_mnemonic,
show_backup_warning,
show_mnemonic,
show_warning,
show_wrong_entry,
)
@ -19,7 +19,7 @@ async def backup_device(ctx, msg):
words = mnemonic.restore()
# warn user about mnemonic safety
await show_warning(ctx)
await show_backup_warning(ctx)
storage.set_unfinished_backup(True)
storage.set_backed_up()

@ -1,8 +1,9 @@
from trezor import config, loop, ui, wire
from trezor import config, ui, wire
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.popup import Popup
from trezor.ui.text import Text
from apps.common.confirm import require_confirm
@ -81,11 +82,10 @@ async def request_pin_ack(ctx, *args, **kwargs):
raise wire.ActionCancelled("Cancelled")
@ui.layout
async def pin_mismatch():
text = Text("PIN mismatch", ui.ICON_WRONG, icon_color=ui.RED)
text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED)
text.normal("Entered PINs do not", "match each other.")
text.normal("")
text.normal("Please, try again...")
text.render()
await loop.sleep(3 * 1000 * 1000)
popup = Popup(text, 3000) # show for 3 seconds
await popup

@ -19,6 +19,9 @@ from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.management.change_pin import request_pin_ack, request_pin_confirm
if __debug__:
from apps.debug import input_signal
async def recovery_device(ctx, msg):
"""
@ -65,7 +68,7 @@ async def recovery_device(ctx, msg):
# ask for pin repeatedly
if msg.pin_protection:
newpin = await request_pin_confirm(ctx, cancellable=False)
newpin = await request_pin_confirm(ctx, allow_cancel=False)
else:
newpin = ""
@ -100,26 +103,31 @@ async def recovery_device(ctx, msg):
return Success(message="Device recovered")
@ui.layout
async def request_wordcount(ctx, title: str) -> int:
await ctx.call(ButtonRequest(code=MnemonicWordCount), ButtonAck)
text = Text(title, ui.ICON_RECOVERY)
text.normal("Number of words?")
count = await ctx.wait(WordSelector(text))
if __debug__:
count = await ctx.wait(WordSelector(text), input_signal)
count = int(count) # if input_signal was triggered, count is a string
else:
count = await ctx.wait(WordSelector(text))
return count
@ui.layout
async def request_mnemonic(ctx, count: int) -> str:
await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck)
words = []
board = MnemonicKeyboard()
for i in range(count):
board.prompt = "Type the %s word:" % format_ordinal(i + 1)
word = await ctx.wait(board)
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(i + 1))
if __debug__:
word = await ctx.wait(keyboard, input_signal)
else:
word = await ctx.wait(keyboard)
words.append(word)
return " ".join(words)

@ -1,21 +1,19 @@
from micropython import const
from ubinascii import hexlify
from trezor import config, ui, wire, workflow
from trezor import config, ui, wire
from trezor.crypto import bip39, hashlib, random
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.EntropyRequest import EntropyRequest
from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.confirm import HoldToConfirmDialog
from trezor.ui.mnemonic import MnemonicKeyboard
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks, format_ordinal
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.common.confirm import hold_to_confirm, require_confirm
from apps.management.change_pin import request_pin_confirm
if __debug__:
@ -31,18 +29,8 @@ async def reset_device(ctx, msg):
if storage.is_initialized():
raise wire.UnexpectedMessage("Already initialized")
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
text.normal("Do you really want to")
text.br()
text.normal("create 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, code=ButtonRequestType.ResetDevice)
# make sure use knows he's setting up a new wallet
await show_reset_warning(ctx)
# request new PIN
if msg.pin_protection:
@ -63,7 +51,7 @@ async def reset_device(ctx, msg):
if not msg.skip_backup and not msg.no_backup:
# require confirmation of the mnemonic safety
await show_warning(ctx)
await show_backup_warning(ctx)
# show mnemonic and require confirmation of a random word
while True:
@ -87,12 +75,9 @@ async def reset_device(ctx, msg):
no_backup=msg.no_backup,
)
# show success message. if we skipped backup, it's possible that homescreen
# is still running, uninterrupted. restart it to pick up new label.
# show success message
if not msg.skip_backup and not msg.no_backup:
await show_success(ctx)
else:
workflow.restartdefault()
return Success(message="Initialized")
@ -105,7 +90,21 @@ def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) ->
return bip39.from_data(entropy[: strength // 8])
async def show_warning(ctx):
async def show_reset_warning(ctx):
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
text.normal("Do you really want to")
text.br()
text.normal("create 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, code=ButtonRequestType.ResetDevice)
async def show_backup_warning(ctx):
text = Text("Backup your seed", ui.ICON_NOCOPY)
text.normal(
"Never make a digital",
@ -119,7 +118,7 @@ async def show_warning(ctx):
async def show_wrong_entry(ctx):
text = Text("Wrong entry!", ui.ICON_WRONG, icon_color=ui.RED)
text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED)
text.normal("You have entered", "wrong seed word.", "Please check again.")
await require_confirm(
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
@ -127,7 +126,7 @@ async def show_wrong_entry(ctx):
async def show_success(ctx):
text = Text("Backup is done!", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
text.normal(
"Never make a digital",
"copy of your recovery",
@ -148,32 +147,33 @@ async def show_entropy(ctx, entropy: bytes):
async def show_mnemonic(ctx, mnemonic: str):
await ctx.call(
ButtonRequest(code=ButtonRequestType.ResetDevice), MessageType.ButtonAck
)
first_page = const(0)
words_per_page = const(4)
words = list(enumerate(mnemonic.split()))
pages = list(chunks(words, words_per_page))
paginator = paginate(show_mnemonic_page, len(pages), first_page, pages)
await ctx.wait(paginator)
# split mnemonic words into pages
PER_PAGE = const(4)
words = mnemonic.split()
words = list(enumerate(words))
words = list(chunks(words, PER_PAGE))
# display the pages, with a confirmation dialog on the last one
pages = [get_mnemonic_page(page) for page in words]
paginated = Paginated(pages)
@ui.layout
async def show_mnemonic_page(page: int, page_count: int, pages: list):
if __debug__:
debug.reset_current_words = [word for _, word in pages[page]]
lines = ["%2d. %s" % (wi + 1, word) for wi, word in pages[page]]
text = Text("Recovery seed", ui.ICON_RESET)
text.mono(*lines)
content = Scrollpage(text, page, page_count)
def export_displayed_words():
# export currently displayed mnemonic words into debuglink
debug.reset_current_words = [w for _, w in words[paginated.page]]
if page + 1 == page_count:
await HoldToConfirmDialog(content)
else:
content.render()
await animate_swipe()
paginated.on_change = export_displayed_words
export_displayed_words()
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
def get_mnemonic_page(words: list):
text = Text("Recovery seed", ui.ICON_RESET)
for index, word in words:
text.mono("%2d. %s" % (index + 1, word))
return text
async def check_mnemonic(ctx, mnemonic: str) -> bool:
@ -192,11 +192,12 @@ async def check_mnemonic(ctx, mnemonic: str) -> bool:
return True
@ui.layout
async def check_word(ctx, words: list, index: int):
if __debug__:
debug.reset_word_index = index
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(index + 1))
result = await ctx.wait(keyboard)
if __debug__:
result = await ctx.wait(keyboard, debug.input_signal)
else:
result = await ctx.wait(keyboard)
return result == words[index]

@ -1,6 +1,8 @@
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.messages.Success import Success
from trezor.ui.button import ButtonCancel
from trezor.ui.loader import LoaderDanger
from trezor.ui.text import Text
from apps.common import storage
@ -8,17 +10,15 @@ from apps.common.confirm import require_hold_to_confirm
async def wipe_device(ctx, msg):
text = Text("Wipe device", ui.ICON_WIPE, icon_color=ui.RED)
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,
code=ButtonRequestType.WipeDevice,
button_style=ui.BTN_CANCEL,
loader_style=ui.LDR_DANGER,
ButtonRequestType.WipeDevice,
confirm_style=ButtonCancel,
loader_style=LoaderDanger,
)
storage.wipe()

@ -1,7 +1,43 @@
from trezor import res, ui, utils
from trezor.messages import ButtonRequestType
from trezor import loop, ui, utils
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.text import Text
from trezor.utils import chunks
if __debug__:
from apps.debug import confirm_signal
async def naive_pagination(
ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5
):
from trezor.ui.scroll import CANCELLED, CONFIRMED, PaginatedWithButtons
pages = []
page_lines = paginate_lines(lines, per_page)
for i, lines in enumerate(page_lines):
if len(page_lines) > 1:
paging = "%s/%s" % (i + 1, len(page_lines))
else:
paging = ""
text = Text("%s %s" % (title, paging), icon, icon_color)
text.normal(*lines)
pages.append(text)
paginated = PaginatedWithButtons(pages, one_by_one=True)
while True:
await ctx.call(
ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck
)
if __debug__:
result = await loop.spawn(paginated, confirm_signal)
else:
result = await paginated
if result is CONFIRMED:
return True
if result is CANCELLED:
return False
def paginate_lines(lines, lines_per_page=5):
@ -28,94 +64,9 @@ def paginate_lines(lines, lines_per_page=5):
return pages
@ui.layout
async def tx_dialog(
ctx,
code,
content,
cancel_btn,
confirm_btn,
cancel_style,
confirm_style,
scroll_tuple=None,
):
from trezor.messages import MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import ConfirmDialog
from trezor.ui.scroll import Scrollpage
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
if scroll_tuple and scroll_tuple[1] > 1:
content = Scrollpage(content, scroll_tuple[0], scroll_tuple[1])
dialog = ConfirmDialog(
content,
cancel=cancel_btn,
confirm=confirm_btn,
cancel_style=cancel_style,
confirm_style=confirm_style,
)
return await ctx.wait(dialog)
async def naive_pagination(
ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5
):
from trezor.ui.confirm import CANCELLED, CONFIRMED, DEFAULT_CANCEL, DEFAULT_CONFIRM
if isinstance(lines, (list, tuple)):
lines = lines
else:
lines = list(chunks(lines, 16))
pages = paginate_lines(lines, per_page)
npages = len(pages)
cur_step = 0
code = ButtonRequestType.SignTx
iback = res.load(ui.ICON_BACK)
inext = res.load(ui.ICON_CLICK)
while cur_step <= npages:
text = pages[cur_step]
fst_page = cur_step == 0
lst_page = cur_step + 1 >= npages
cancel_btn = DEFAULT_CANCEL if fst_page else iback
cancel_style = ui.BTN_CANCEL if fst_page else ui.BTN_DEFAULT
confirm_btn = DEFAULT_CONFIRM if lst_page else inext
confirm_style = ui.BTN_CONFIRM if lst_page else ui.BTN_DEFAULT
paging = ("%d/%d" % (cur_step + 1, npages)) if npages > 1 else ""
content = Text("%s %s" % (title, paging), icon, icon_color=icon_color)
content.normal(*text)
reaction = await tx_dialog(
ctx,
code,
content,
cancel_btn,
confirm_btn,
cancel_style,
confirm_style,
(cur_step, npages),
)
if fst_page and reaction == CANCELLED:
return False
elif not lst_page and reaction == CONFIRMED:
cur_step += 1
elif lst_page and reaction == CONFIRMED:
return True
elif reaction == CANCELLED:
cur_step -= 1
elif reaction == CONFIRMED:
cur_step += 1
def format_amount(value):
return "%s XMR" % utils.format_amount(value, 12)
def split_address(address):
return chunks(address, 16)
return utils.chunks(address, 16)

@ -2,6 +2,7 @@ from ubinascii import hexlify
from trezor import ui, wire
from trezor.messages import ButtonRequestType
from trezor.ui.popup import Popup
from trezor.ui.text import Text
from trezor.utils import chunks
@ -12,25 +13,25 @@ DUMMY_PAYMENT_ID = b"\x00" * 8
async def require_confirm_watchkey(ctx):
content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN)
content = Text("Confirm export", ui.ICON_SEND, ui.GREEN)
content.normal("Do you really want to", "export watch-only", "credentials?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_keyimage_sync(ctx):
content = Text("Confirm ki sync", ui.ICON_SEND, icon_color=ui.GREEN)
content = Text("Confirm ki sync", ui.ICON_SEND, ui.GREEN)
content.normal("Do you really want to", "sync key images?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_live_refresh(ctx):
content = Text("Confirm refresh", ui.ICON_SEND, icon_color=ui.GREEN)
content = Text("Confirm refresh", ui.ICON_SEND, ui.GREEN)
content.normal("Do you really want to", "start refresh?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_tx_key(ctx, export_key=False):
content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN)
content = Text("Confirm export", ui.ICON_SEND, ui.GREEN)
txt = ["Do you really want to"]
if export_key:
txt.append("export tx_key?")
@ -111,14 +112,55 @@ async def _require_confirm_payment_id(ctx, payment_id):
async def _require_confirm_fee(ctx, fee):
content = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN)
content = Text("Confirm fee", ui.ICON_SEND, ui.GREEN)
content.bold(common.format_amount(fee))
await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput)
@ui.layout_no_slide
class TransactionStep(ui.Control):
def __init__(self, state, info):
self.state = state
self.info = info
def on_render(self):
state = self.state
info = self.info
ui.header("Signing transaction", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
p = 1000 * state.progress_cur // state.progress_total
ui.display.loader(p, False, -4, ui.WHITE, ui.BG)
ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG)
if len(info) > 1:
ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG)
class KeyImageSyncStep(ui.Control):
def __init__(self, current, total_num):
self.current = current
self.total_num = total_num
def on_render(self):
current = self.current
total_num = self.total_num
ui.header("Syncing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
p = (1000 * (current + 1) // total_num) if total_num > 0 else 0
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
class LiveRefreshStep(ui.Control):
def __init__(self, current):
self.current = current
def on_render(self):
current = self.current
ui.header("Refreshing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
p = (1000 * current // 8) % 1000
ui.display.loader(p, True, 18, ui.WHITE, ui.BG)
ui.display.text_center(
ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG
)
async def transaction_step(state, step, sub_step=None):
info = []
if step == 0:
info = ["Signing..."]
elif step == 100:
@ -139,43 +181,16 @@ async def transaction_step(state, step, sub_step=None):
info = ["Processing..."]
state.progress_cur += 1
ui.display.clear()
text = Text("Signing transaction", ui.ICON_SEND, icon_color=ui.BLUE)
text.render()
p = 1000 * state.progress_cur // state.progress_total
ui.display.loader(p, False, -4, ui.WHITE, ui.BG)
ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG)
if len(info) > 1:
ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG)
ui.display.refresh()
await Popup(TransactionStep(state, info))
@ui.layout_no_slide
async def keyimage_sync_step(ctx, current, total_num):
if current is None:
return
ui.display.clear()
text = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE)
text.render()
await Popup(KeyImageSyncStep(current, total_num))
p = (1000 * (current + 1) // total_num) if total_num > 0 else 0
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
ui.display.refresh()
@ui.layout_no_slide
async def live_refresh_step(ctx, current):
if current is None:
return
ui.display.clear()
text = Text("Refreshing", ui.ICON_SEND, icon_color=ui.BLUE)
text.render()
step = 8
p = (1000 * current // step) % 1000
ui.display.loader(p, True, 18, ui.WHITE, ui.BG)
ui.display.text_center(ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG)
ui.display.refresh()
await Popup(LiveRefreshStep(current))

@ -10,7 +10,7 @@ from apps.common.confirm import require_confirm, require_hold_to_confirm
async def require_confirm_text(ctx, action: str):
content = action.split(" ")
text = Text("Confirm action", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False)
text = Text("Confirm action", ui.ICON_SEND, ui.GREEN, new_lines=False)
text.normal(*content)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -26,13 +26,13 @@ async def require_confirm_fee(ctx, action: str, fee: int):
async def require_confirm_content(ctx, headline: str, content: list):
text = Text(headline, ui.ICON_SEND, icon_color=ui.GREEN)
text = Text(headline, ui.ICON_SEND, ui.GREEN)
text.normal(*content)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_final(ctx, fee: int):
text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Final confirm", ui.ICON_SEND, ui.GREEN)
text.normal("Sign this transaction")
text.bold("and pay %s XEM" % format_amount(fee, NEM_MAX_DIVISIBILITY))
text.normal("for network fee?")

@ -1,5 +1,3 @@
from micropython import const
from trezor import ui, wire
from trezor.messages import (
NEMMosaicCreation,
@ -9,8 +7,8 @@ from trezor.messages import (
NEMSupplyChangeType,
NEMTransactionCommon,
)
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.confirm import CONFIRMED, Confirm
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from ..layout import (
@ -22,6 +20,9 @@ from ..layout import (
from apps.common.layout import split_address
if __debug__:
from apps.debug import confirm_signal
async def ask_mosaic_creation(
ctx, common: NEMTransactionCommon, creation: NEMMosaicCreation
@ -76,21 +77,16 @@ def _supply_message(supply_change):
async def _require_confirm_properties(ctx, definition: NEMMosaicDefinition):
# TODO: we should send a button request here
properties = _get_mosaic_properties(definition)
first_page = const(0)
paginator = paginate(_show_page, len(properties), first_page, properties)
await ctx.wait(paginator)
@ui.layout
async def _show_page(page: int, page_count: int, content):
content = Scrollpage(content[page], page, page_count)
if page + 1 == page_count:
if await ConfirmDialog(content) != CONFIRMED:
raise wire.ActionCancelled("Action cancelled")
pages = _get_mosaic_properties(definition)
pages[-1] = Confirm(pages[-1])
paginated = Paginated(pages)
if __debug__:
result = await ctx.wait(paginated, confirm_signal)
else:
content.render()
await animate_swipe()
result = await ctx.wait(paginated)
if result is not CONFIRMED:
raise wire.ActionCancelled("Action cancelled")
def _get_mosaic_properties(definition: NEMMosaicDefinition):

@ -53,7 +53,7 @@ async def ask_aggregate_modification(
async def _require_confirm_address(ctx, action: str, address: str):
text = Text("Confirm address", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm address", ui.ICON_SEND, ui.GREEN)
text.normal(action)
text.mono(*split_address(address))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)

@ -48,7 +48,7 @@ async def ask_transfer_mosaic(
mosaic_quantity = mosaic.quantity * transfer.amount / NEM_MOSAIC_AMOUNT_DIVISOR
if definition:
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
msg.normal("Confirm transfer of")
msg.bold(
format_amount(mosaic_quantity, definition["divisibility"])
@ -60,20 +60,20 @@ async def ask_transfer_mosaic(
if "levy" in definition and "fee" in definition:
levy_msg = _get_levy_msg(definition, mosaic_quantity, common.network)
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
msg.normal("Confirm mosaic", "levy fee of")
msg.bold(levy_msg)
await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput)
else:
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.RED)
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.RED)
msg.bold("Unknown mosaic!")
msg.normal("Divisibility and levy")
msg.normal("cannot be shown for")
msg.normal("unknown mosaics")
await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput)
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
msg.normal("Confirm transfer of")
msg.bold("%s raw units" % mosaic_quantity)
msg.normal("of")
@ -121,7 +121,7 @@ async def ask_importance_transfer(
async def _require_confirm_transfer(ctx, recipient, value):
text = Text("Confirm transfer", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transfer", ui.ICON_SEND, ui.GREEN)
text.bold("Send %s XEM" % format_amount(value, NEM_MAX_DIVISIBILITY))
text.normal("to")
text.mono(*split_address(recipient))
@ -132,11 +132,11 @@ async def _require_confirm_payload(ctx, payload: bytearray, encrypt=False):
payload = bytes(payload).decode()
if encrypt:
text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm payload", ui.ICON_SEND, ui.GREEN)
text.bold("Encrypted:")
text.normal(*payload.split(" "))
else:
text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.RED)
text = Text("Confirm payload", ui.ICON_SEND, ui.RED)
text.bold("Unencrypted:")
text.normal(*payload.split(" "))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)

@ -10,14 +10,14 @@ from apps.common.layout import split_address
async def require_confirm_fee(ctx, fee):
text = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm fee", ui.ICON_SEND, ui.GREEN)
text.normal("Transaction fee:")
text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP")
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_destination_tag(ctx, tag):
text = Text("Confirm tag", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm tag", ui.ICON_SEND, ui.GREEN)
text.normal("Destination tag:")
text.bold(str(tag))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -25,7 +25,7 @@ async def require_confirm_destination_tag(ctx, tag):
async def require_confirm_tx(ctx, to, value):
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.bold(format_amount(value, helpers.DIVISIBILITY) + " XRP")
text.normal("to")
text.mono(*split_address(to))

@ -9,7 +9,7 @@ from apps.stellar import consts
async def require_confirm_init(
ctx, address: str, network_passphrase: str, accounts_match: bool
):
text = Text("Confirm Stellar", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm Stellar", ui.ICON_SEND, ui.GREEN)
text.normal("Initialize signing with")
if accounts_match:
text.normal("your account")
@ -19,14 +19,14 @@ async def require_confirm_init(
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
network = get_network_warning(network_passphrase)
if network:
text = Text("Confirm network", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm network", ui.ICON_CONFIRM, ui.GREEN)
text.normal("Transaction is on")
text.bold(network)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_memo(ctx, memo_type: int, memo_text: str):
text = Text("Confirm memo", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm memo", ui.ICON_CONFIRM, ui.GREEN)
if memo_type == consts.MEMO_TYPE_TEXT:
text.bold("Memo (TEXT)")
elif memo_type == consts.MEMO_TYPE_ID:
@ -47,7 +47,7 @@ async def require_confirm_final(ctx, fee: int, num_operations: int):
op_str = str(num_operations) + " operation"
if num_operations > 1:
op_str += "s"
text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Final confirm", ui.ICON_SEND, ui.GREEN)
text.normal("Sign this transaction")
text.normal("made up of " + op_str)
text.bold("and pay " + format_amount(fee))

@ -21,7 +21,7 @@ from apps.stellar.layout import format_amount, require_confirm, split, trim_to_r
async def confirm_source_account(ctx, source_account: bytes):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Source account:")
text.mono(*split(source_account))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -32,7 +32,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
t = "Allow Trust"
else:
t = "Revoke Trust"
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold(t)
text.normal("of %s by:" % op.asset_code)
text.mono(*split(trim_to_rows(op.trusted_account, 3)))
@ -41,7 +41,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Account Merge")
text.normal("All XLM will be sent to:")
text.mono(*split(trim_to_rows(op.destination_account, 3)))
@ -49,7 +49,7 @@ async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp):
async def confirm_bump_sequence_op(ctx, op: StellarBumpSequenceOp):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Bump Sequence")
text.normal("Set sequence to")
text.mono(str(op.bump_to))
@ -61,7 +61,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
t = "Delete Trust"
else:
t = "Add Trust"
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold(t)
text.normal("Asset: %s" % op.asset.code)
text.normal("Amount: %s" % format_amount(op.limit, ticker=False))
@ -70,7 +70,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
async def confirm_create_account_op(ctx, op: StellarCreateAccountOp):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Create Account")
text.normal("with %s" % format_amount(op.starting_balance))
text.mono(*split(trim_to_rows(op.new_account, 3)))
@ -98,7 +98,7 @@ async def confirm_manage_offer_op(ctx, op: StellarManageOfferOp):
async def _confirm_offer(ctx, title, op):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold(title)
text.normal(
"Sell %s %s" % (format_amount(op.amount, ticker=False), op.selling_asset.code)
@ -117,28 +117,28 @@ async def confirm_manage_data_op(ctx, op: StellarManageDataOp):
title = "Set"
else:
title = "Clear"
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("%s data value key" % title)
text.mono(*split(op.key))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.value:
digest = sha256(op.value).digest()
digest_str = hexlify(digest).decode()
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Value (SHA-256):")
text.mono(*split(digest_str))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Path Pay %s" % format_amount(op.destination_amount, ticker=False))
text.bold("%s to:" % format_asset_code(op.destination_asset))
text.mono(*split(trim_to_rows(op.destination_account, 3)))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
await confirm_asset_issuer(ctx, op.destination_asset)
# confirm what the sender is using to pay
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.normal("Pay using")
text.bold(format_amount(op.send_max, ticker=False))
text.bold(format_asset_code(op.send_asset))
@ -149,7 +149,7 @@ async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp):
async def confirm_payment_op(ctx, op: StellarPaymentOp):
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Pay %s" % format_amount(op.amount, ticker=False))
text.bold("%s to:" % format_asset_code(op.asset))
text.mono(*split(trim_to_rows(op.destination_account, 3)))
@ -159,30 +159,30 @@ async def confirm_payment_op(ctx, op: StellarPaymentOp):
async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
if op.inflation_destination_account:
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Set Inflation Destination")
text.mono(*split(op.inflation_destination_account))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.clear_flags:
t = _format_flags(op.clear_flags)
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Clear Flags")
text.mono(*t)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.set_flags:
t = _format_flags(op.set_flags)
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Set Flags")
text.mono(*t)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
thresholds = _format_thresholds(op)
if thresholds:
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Account Thresholds")
text.mono(*thresholds)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.home_domain:
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold("Home Domain")
text.mono(*split(op.home_domain))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -192,7 +192,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
else:
t = "Remove Signer (%s)"
if op.signer_type == consts.SIGN_TYPE_ACCOUNT:
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold(t % "acc")
text.mono(*split(helpers.address_from_public_key(op.signer_key)))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -201,7 +201,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
signer_type = "auth"
else:
signer_type = "hash"
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
text.bold(t % signer_type)
text.mono(*split(hexlify(op.signer_key).decode()))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -244,7 +244,7 @@ def format_asset_code(asset: StellarAssetType) -> str:
async def confirm_asset_issuer(ctx, asset: StellarAssetType):
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE:
return
text = Text("Confirm issuer", ui.ICON_CONFIRM, icon_color=ui.GREEN)
text = Text("Confirm issuer", ui.ICON_CONFIRM, ui.GREEN)
text.bold("%s issuer:" % asset.code)
text.mono(*split(asset.issuer))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)

@ -26,6 +26,6 @@ async def get_public_key(ctx, msg, keychain):
async def _show_tezos_pubkey(ctx, pubkey):
lines = chunks(pubkey, 18)
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
text.mono(*lines)
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)

@ -1,10 +1,6 @@
from micropython import const
from trezor import ui, wire
from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.confirm import CANCELLED, ConfirmDialog
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.scroll import Paginated
from trezor.ui.text import Text
from trezor.utils import chunks, format_amount
@ -13,7 +9,7 @@ from apps.tezos.helpers import TEZOS_AMOUNT_DIVISIBILITY
async def require_confirm_tx(ctx, to, value):
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.bold(format_tezos_amount(value))
text.normal("to")
text.mono(*split_address(to))
@ -21,7 +17,7 @@ async def require_confirm_tx(ctx, to, value):
async def require_confirm_fee(ctx, value, fee):
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
text.normal("Amount:")
text.bold(format_tezos_amount(value))
text.normal("Fee:")
@ -30,14 +26,14 @@ async def require_confirm_fee(ctx, value, fee):
async def require_confirm_origination(ctx, address):
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE)
text.normal("Address:")
text.mono(*split_address(address))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_origination_fee(ctx, balance, fee):
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE)
text.normal("Balance:")
text.bold(format_tezos_amount(balance))
text.normal("Fee:")
@ -46,21 +42,21 @@ async def require_confirm_origination_fee(ctx, balance, fee):
async def require_confirm_delegation_baker(ctx, baker):
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE)
text.normal("Baker address:")
text.mono(*split_address(baker))
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_set_delegate(ctx, fee):
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE)
text.normal("Fee:")
text.bold(format_tezos_amount(fee))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_register_delegate(ctx, address, fee):
text = Text("Register delegate", ui.ICON_SEND, icon_color=ui.BLUE)
text = Text("Register delegate", ui.ICON_SEND, ui.BLUE)
text.bold("Fee: " + format_tezos_amount(fee))
text.normal("Address:")
text.mono(*split_address(address))
@ -88,28 +84,18 @@ async def require_confirm_ballot(ctx, proposal, ballot):
await require_confirm(ctx, text, ButtonRequestType.SignTx)
# use, when there are more then one proposals in one operation
async def require_confirm_proposals(ctx, proposals):
await ctx.call(ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck)
first_page = const(0)
pages = proposals
title = "Submit proposals" if len(proposals) > 1 else "Submit proposal"
paginator = paginate(show_proposal_page, len(pages), first_page, pages, title)
return await ctx.wait(paginator)
@ui.layout
async def show_proposal_page(page: int, page_count: int, pages: list, title: str):
text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE)
text.bold("Proposal {}: ".format(page + 1))
text.mono(*split_proposal(pages[page]))
content = Scrollpage(text, page, page_count)
if page + 1 >= page_count:
confirm = await ConfirmDialog(content)
if confirm == CANCELLED:
raise wire.ActionCancelled("Cancelled")
if len(proposals) > 1:
title = "Submit proposals"
else:
content.render()
await animate_swipe()
title = "Submit proposal"
pages = []
for page, proposal in enumerate(proposals):
text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE)
text.bold("Proposal {}: ".format(page + 1))
text.mono(*split_proposal(proposal))
pages.append(text)
paginated = Paginated(pages)
await require_confirm(ctx, paginated, ButtonRequestType.SignTx)

@ -29,19 +29,19 @@ async def confirm_output(ctx, output, coin):
data = output.op_return_data
if omni.is_valid(data):
# OMNI transaction
text = Text("OMNI transaction", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("OMNI transaction", ui.ICON_SEND, ui.GREEN)
text.normal(omni.parse(data))
else:
# generic OP_RETURN
data = hexlify(data).decode()
if len(data) >= 18 * 5:
data = data[: (18 * 5 - 3)] + "..."
text = Text("OP_RETURN", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("OP_RETURN", ui.ICON_SEND, ui.GREEN)
text.mono(*split_op_return(data))
else:
address = output.address
address_short = addresses.address_short(coin, address)
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
text.normal(format_coin_amount(output.amount, coin) + " to")
text.mono(*split_address(address_short))
return await confirm(ctx, text, ButtonRequestType.ConfirmOutput)

@ -383,37 +383,32 @@ class ConfirmState:
workflow.onclose(self.workflow)
self.workflow = None
@ui.layout
async def confirm_layout(self) -> None:
workflow.webauthn_stop_signal.reset()
await loop.spawn(self.confirm_layout_inner(), workflow.webauthn_stop_signal)
async def confirm_layout_inner(self) -> None:
from trezor.ui.confirm import ConfirmDialog, CONFIRMED
from trezor.ui.confirm import Confirm, CONFIRMED
from trezor.ui.text import Text
app_id = bytes(self.app_id) # could be bytearray, which doesn't have __hash__
if app_id == _BOGUS_APPID and self.action == _CONFIRM_REGISTER:
text = Text("U2F", ui.ICON_WRONG, icon_color=ui.RED)
text = Text("U2F", ui.ICON_WRONG, ui.RED)
text.normal(
"Another U2F device", "was used to register", "in this application."
)
text.render()
dialog = ConfirmDialog(text)
dialog = Confirm(text)
else:
content = ConfirmContent(self.action, app_id)
dialog = ConfirmDialog(content)
dialog = Confirm(content)
self.confirmed = await dialog == CONFIRMED
self.confirmed = await dialog is CONFIRMED
class ConfirmContent(ui.Widget):
class ConfirmContent(ui.Control):
def __init__(self, action: int, app_id: bytes) -> None:
self.action = action
self.app_id = app_id
self.app_name = None
self.app_icon = None
self.repaint = True
self.boot()
def boot(self) -> None:
@ -439,14 +434,18 @@ class ConfirmContent(ui.Widget):
self.app_name = name
self.app_icon = icon
def render(self) -> None:
if self.action == _CONFIRM_REGISTER:
header = "U2F Register"
else:
header = "U2F Authenticate"
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon)
ui.display.text_center(ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG)
def on_render(self) -> None:
if self.repaint:
if self.action == _CONFIRM_REGISTER:
header = "U2F Register"
else:
header = "U2F Authenticate"
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon)
ui.display.text_center(
ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG
)
self.repaint = False
def dispatch_cmd(req: Cmd, state: ConfirmState) -> Cmd:

@ -14,7 +14,7 @@ async def bootscreen():
storage.init_unlocked()
return
await lockscreen()
label = None
label = "Enter your PIN"
while True:
pin = await request_pin(label, config.get_pin_rem())
if config.unlock(pin_to_int(pin)):
@ -35,7 +35,7 @@ async def lockscreen():
if not image:
image = res.load("apps/homescreen/res/bg.toif")
await ui.backlight_slide(ui.BACKLIGHT_DIM)
ui.backlight_fade(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.display.avatar(48, 48, image, ui.TITLE_GREY, ui.BG)
@ -50,12 +50,13 @@ async def lockscreen():
)
ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG)
await ui.backlight_slide(ui.BACKLIGHT_NORMAL)
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
await ui.click()
ui.display.backlight(ui.BACKLIGHT_NONE)
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
config.init(show_pin_timeout)
loop.schedule(bootscreen())
loop.run()

@ -18,6 +18,7 @@ after_step_hook = None # function, called after each task step
_QUEUE_SIZE = const(64) # maximum number of scheduled tasks
_queue = utimeq.utimeq(_QUEUE_SIZE)
_paused = {}
_finalizers = {}
if __debug__:
# for performance stats
@ -28,13 +29,15 @@ if __debug__:
log_delay_rb = array.array("i", [0] * log_delay_rb_len)
def schedule(task, value=None, deadline=None):
def schedule(task, value=None, deadline=None, finalizer=None):
"""
Schedule task to be executed with `value` on given `deadline` (in
microseconds). Does not start the event loop itself, see `run`.
"""
if deadline is None:
deadline = utime.ticks_us()
if finalizer is not None:
_finalizers[id(task)] = finalizer
_queue.push(deadline, task, value)
@ -45,11 +48,18 @@ def pause(task, iface):
tasks.add(task)
def finalize(task, value):
fn = _finalizers.pop(id(task), None)
if fn is not None:
fn(task, value)
def close(task):
for iface in _paused:
_paused[iface].discard(task)
_queue.discard(task)
task.close()
finalize(task, GeneratorExit())
def run():
@ -93,16 +103,18 @@ def run():
def _step(task, value):
try:
if isinstance(value, Exception):
if isinstance(value, BaseException):
result = task.throw(value)
else:
result = task.send(value)
except StopIteration: # as e:
except StopIteration as e: # as e:
if __debug__:
log.debug(__name__, "finish: %s", task)
finalize(task, e.value)
except Exception as e:
if __debug__:
log.exception(__name__, e)
finalize(task, e)
else:
if isinstance(result, Syscall):
result.handle(task)
@ -213,6 +225,9 @@ class signal(Syscall):
raise
_type_gen = type((lambda: (yield))())
class spawn(Syscall):
"""
Execute one or more children tasks and wait until one of them exits.
@ -241,39 +256,41 @@ class spawn(Syscall):
def __init__(self, *children, exit_others=True):
self.children = children
self.exit_others = exit_others
self.scheduled = None # list of scheduled wrapper tasks
self.finished = None # list of children that finished
self.scheduled = [] # list of scheduled tasks
self.finished = [] # list of children that finished
self.callback = None
def handle(self, task):
self.callback = task
self.finished = []
self.scheduled = []
for index, child in enumerate(self.children):
parent = self._wait(child, index)
schedule(parent)
self.scheduled.append(parent)
def exit(self, skip_index=-1):
for index, parent in enumerate(self.scheduled):
if index != skip_index:
close(parent)
async def _wait(self, child, index):
try:
result = await child
except Exception as e:
self._finish(child, index, e)
if __debug__:
log.exception(__name__, e)
else:
self._finish(child, index, result)
finalizer = self._finish
scheduled = self.scheduled
finished = self.finished
def _finish(self, child, index, result):
self.callback = task
scheduled.clear()
finished.clear()
for child in self.children:
if isinstance(child, _type_gen):
child_task = child
else:
child_task = iter(child)
schedule(child_task, None, None, finalizer)
scheduled.append(child_task)
def exit(self, except_for=None):
for task in self.scheduled:
if task != except_for:
close(task)
def _finish(self, task, result):
if not self.finished:
for index, child_task in enumerate(self.scheduled):
if child_task is task:
child = self.children[index]
break
self.finished.append(child)
if self.exit_others:
self.exit(index)
self.exit(task)
schedule(self.callback, result)
def __iter__(self):
@ -284,66 +301,3 @@ class spawn(Syscall):
# close() or throw(), kill the children tasks and re-raise
self.exit()
raise
class put(Syscall):
def __init__(self, ch, value=None):
self.ch = ch
self.value = value
def __call__(self, value):
self.value = value
return self
def handle(self, task):
self.ch.schedule_put(schedule, task, self.value)
class take(Syscall):
def __init__(self, ch):
self.ch = ch
def __call__(self):
return self
def handle(self, task):
if self.ch.schedule_take(schedule, task) and self.ch.id is not None:
pause(self.ch, self.ch.id)
class chan:
def __init__(self, id=None):
self.id = id
self.putters = []
self.takers = []
self.put = put(self)
self.take = take(self)
def schedule_publish(self, schedule, value):
if self.takers:
for taker in self.takers:
schedule(taker, value)
self.takers.clear()
return True
else:
return False
def schedule_put(self, schedule, putter, value):
if self.takers:
taker = self.takers.pop(0)
schedule(taker, value)
schedule(putter, value)
return True
else:
self.putters.append((putter, value))
return False
def schedule_take(self, schedule, taker):
if self.putters:
putter, value = self.putters.pop(0)
schedule(taker, value)
schedule(putter, value)
return True
else:
self.takers.append(taker)
return False

@ -31,6 +31,10 @@ SIZE = Display.FONT_SIZE
WIDTH = Display.WIDTH
HEIGHT = Display.HEIGHT
# viewport margins
VIEWX = const(6)
VIEWY = const(9)
def lerpi(a: int, b: int, t: float) -> int:
return int(a + t * (b - a))
@ -55,43 +59,9 @@ from trezor.ui import style # isort:skip
from trezor.ui.style import * # isort:skip # noqa: F401,F403
def contains(area: tuple, pos: tuple) -> bool:
x, y = pos
ax, ay, aw, ah = area
return ax <= x <= ax + aw and ay <= y <= ay + ah
def rotate(pos: tuple) -> tuple:
r = display.orientation()
if r == 0:
return pos
x, y = pos
if r == 90:
return (y, WIDTH - x)
if r == 180:
return (WIDTH - x, HEIGHT - y)
if r == 270:
return (HEIGHT - y, x)
def pulse(delay: int):
while True:
# normalize sin from interval -1:1 to 0:1
yield 0.5 + 0.5 * math.sin(utime.ticks_us() / delay)
async def alert(count: int = 3):
short_sleep = loop.sleep(20000)
long_sleep = loop.sleep(80000)
current = display.backlight()
for i in range(count * 2):
if i % 2 == 0:
display.backlight(style.BACKLIGHT_MAX)
yield short_sleep
else:
display.backlight(style.BACKLIGHT_NORMAL)
yield long_sleep
display.backlight(current)
# normalize sin from interval -1:1 to 0:1
return 0.5 + 0.5 * math.sin(utime.ticks_us() / delay)
async def click() -> tuple:
@ -107,53 +77,22 @@ async def click() -> tuple:
return pos
async def backlight_slide(val: int, delay: int = 35000, step: int = 20):
sleep = loop.sleep(delay)
def backlight_fade(val: int, delay: int = 14000, step: int = 15):
if __debug__:
if utils.DISABLE_FADE:
display.backlight(val)
return
current = display.backlight()
for i in range(current, val, -step if current > val else step):
display.backlight(i)
yield sleep
def backlight_slide_sync(val: int, delay: int = 35000, step: int = 20):
current = display.backlight()
for i in range(current, val, -step if current > val else step):
if current > val:
step = -step
for i in range(current, val, step):
display.backlight(i)
utime.sleep_us(delay)
def layout(f):
async def inner(*args, **kwargs):
await backlight_slide(style.BACKLIGHT_DIM)
slide = backlight_slide(style.BACKLIGHT_NORMAL)
try:
layout = f(*args, **kwargs)
workflow.onlayoutstart(layout)
loop.schedule(slide)
display.clear()
return await layout
finally:
loop.close(slide)
workflow.onlayoutclose(layout)
return inner
def layout_no_slide(f):
async def inner(*args, **kwargs):
try:
layout = f(*args, **kwargs)
workflow.onlayoutstart(layout)
return await layout
finally:
workflow.onlayoutclose(layout)
return inner
def header(
title: str,
icon: bytes = style.ICON_DEFAULT,
icon: str = style.ICON_DEFAULT,
fg: int = style.FG,
bg: int = style.BG,
ifg: int = style.GREEN,
@ -163,10 +102,6 @@ def header(
display.text(44, 35, title, BOLD, fg, bg)
VIEWX = const(6)
VIEWY = const(9)
def grid(
i: int,
n_x: int = 3,
@ -186,23 +121,90 @@ def grid(
return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y)
class Widget:
tainted = True
def in_area(area: tuple, x: int, y: int) -> bool:
ax, ay, aw, ah = area
return ax <= x <= ax + aw and ay <= y <= ay + ah
def taint(self):
self.tainted = True
# render events
RENDER = const(-255)
REPAINT = const(-256)
class Control:
def dispatch(self, event, x, y):
if event is RENDER:
self.on_render()
elif event is io.TOUCH_START:
self.on_touch_start(x, y)
elif event is io.TOUCH_MOVE:
self.on_touch_move(x, y)
elif event is io.TOUCH_END:
self.on_touch_end(x, y)
elif event is REPAINT:
self.repaint = True
def on_render(self):
pass
def on_touch_start(self, x, y):
pass
def render(self):
def on_touch_move(self, x, y):
pass
def touch(self, event, pos):
def on_touch_end(self, x, y):
pass
def __iter__(self):
_RENDER_DELAY_US = const(10000) # 10 msec
class LayoutCancelled(Exception):
pass
class Result(Exception):
def __init__(self, value):
self.value = value
class Layout(Control):
"""
"""
async def __iter__(self):
value = None
try:
if workflow.layout_signal.task is not None:
workflow.layout_signal.send(LayoutCancelled())
workflow.onlayoutstart(self)
while True:
layout_tasks = self.create_tasks()
await loop.spawn(workflow.layout_signal, *layout_tasks)
except Result as result:
value = result.value
finally:
workflow.onlayoutclose(self)
return value
def create_tasks(self):
return self.handle_input(), self.handle_rendering()
def handle_input(self):
touch = loop.wait(io.TOUCH)
result = None
while result is None:
self.render()
event, *pos = yield touch
result = self.touch(event, pos)
return result
while True:
event, x, y = yield touch
self.dispatch(event, x, y)
self.dispatch(RENDER, 0, 0)
def handle_rendering(self):
backlight_fade(style.BACKLIGHT_DIM)
display.clear()
self.dispatch(RENDER, 0, 0)
display.refresh()
backlight_fade(style.BACKLIGHT_NORMAL)
sleep = loop.sleep(_RENDER_DELAY_US)
while True:
self.dispatch(RENDER, 0, 0)
yield sleep

@ -1,115 +1,213 @@
from micropython import const
from trezor import io, ui
from trezor.ui import Widget, contains, display, rotate
from trezor import ui
from trezor.ui import display, in_area
class ButtonDefault:
class normal:
bg_color = ui.BLACKISH
fg_color = ui.FG
text_style = ui.BOLD
border_color = ui.BG
radius = ui.RADIUS
class active:
bg_color = ui.FG
fg_color = ui.BLACKISH
text_style = ui.BOLD
border_color = ui.FG
radius = ui.RADIUS
class disabled:
bg_color = ui.BG
fg_color = ui.GREY
text_style = ui.NORMAL
border_color = ui.BG
radius = ui.RADIUS
class ButtonMono(ButtonDefault):
class normal(ButtonDefault.normal):
text_style = ui.MONO
class active(ButtonDefault.active):
text_style = ui.MONO
class disabled(ButtonDefault.disabled):
text_style = ui.MONO
class ButtonMonoDark:
class normal:
bg_color = ui.DARK_BLACK
fg_color = ui.DARK_WHITE
text_style = ui.MONO
border_color = ui.BG
radius = ui.RADIUS
class active:
bg_color = ui.FG
fg_color = ui.DARK_BLACK
text_style = ui.MONO
border_color = ui.FG
radius = ui.RADIUS
class disabled:
bg_color = ui.DARK_BLACK
fg_color = ui.GREY
text_style = ui.MONO
border_color = ui.BG
radius = ui.RADIUS
class ButtonConfirm(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.GREEN
class active(ButtonDefault.active):
fg_color = ui.GREEN
class ButtonCancel(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.RED
class active(ButtonDefault.active):
fg_color = ui.RED
class ButtonClear(ButtonDefault):
class normal(ButtonDefault.normal):
bg_color = ui.ORANGE
class active(ButtonDefault.active):
fg_color = ui.ORANGE
class ButtonMonoConfirm(ButtonDefault):
class normal(ButtonDefault.normal):
text_style = ui.MONO
bg_color = ui.GREEN
class active(ButtonDefault.active):
text_style = ui.MONO
fg_color = ui.GREEN
class disabled(ButtonDefault.disabled):
text_style = ui.MONO
# button events
BTN_CLICKED = const(1)
# button states
BTN_INITIAL = const(0)
BTN_DISABLED = const(1)
BTN_FOCUSED = const(2)
BTN_ACTIVE = const(3)
_INITIAL = const(0)
_PRESSED = const(1)
_RELEASED = const(2)
_DISABLED = const(3)
# constants
ICON = const(16) # icon size in pixels
BORDER = const(4) # border size in pixels
# button constants
_ICON = const(16) # icon size in pixels
_BORDER = const(4) # border size in pixels
class Button(Widget):
def __init__(self, area: tuple, content: str, style: dict = ui.BTN_KEY):
class Button(ui.Control):
def __init__(self, area, content, style=ButtonDefault):
self.area = area
self.content = content
self.normal_style = style["normal"] or ui.BTN_KEY["normal"]
self.active_style = style["active"] or ui.BTN_KEY["active"]
self.disabled_style = style["disabled"] or ui.BTN_KEY["disabled"]
self.state = BTN_INITIAL
self.normal_style = style.normal
self.active_style = style.active
self.disabled_style = style.disabled
self.state = _INITIAL
self.repaint = True
def enable(self):
if self.state == BTN_DISABLED:
self.state = BTN_INITIAL
self.tainted = True
if self.state is not _INITIAL:
self.state = _INITIAL
self.repaint = True
def disable(self):
if self.state != BTN_DISABLED:
self.state = BTN_DISABLED
self.tainted = True
def is_enabled(self):
return self.state != BTN_DISABLED
def render(self):
if not self.tainted:
return
state = self.state
if state == BTN_DISABLED:
s = self.disabled_style
elif state == BTN_ACTIVE:
s = self.active_style
else:
s = self.normal_style
ax, ay, aw, ah = self.area
self.render_background(s, ax, ay, aw, ah)
self.render_content(s, ax, ay, aw, ah)
self.tainted = False
if self.state is not _DISABLED:
self.state = _DISABLED
self.repaint = True
def on_render(self):
if self.repaint:
if self.state is _DISABLED:
s = self.disabled_style
elif self.state is _PRESSED:
s = self.active_style
else:
s = self.normal_style
ax, ay, aw, ah = self.area
self.render_background(s, ax, ay, aw, ah)
self.render_content(s, ax, ay, aw, ah)
self.repaint = False
def render_background(self, s, ax, ay, aw, ah):
radius = s["radius"]
bg_color = s["bg-color"]
border_color = s["border-color"]
if border_color != bg_color:
radius = s.radius
bg_color = s.bg_color
border_color = s.border_color
if border_color == bg_color:
# we don't need to render the border
display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius)
else:
# render border and background on top of it
display.bar_radius(ax, ay, aw, ah, border_color, ui.BG, radius)
display.bar_radius(
ax + BORDER,
ay + BORDER,
aw - BORDER * 2,
ah - BORDER * 2,
ax + _BORDER,
ay + _BORDER,
aw - _BORDER * 2,
ah - _BORDER * 2,
bg_color,
border_color,
radius,
)
else:
# render only the background
display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius)
def render_content(self, s, ax, ay, aw, ah):
c = self.content
tx = ax + aw // 2
ty = ay + ah // 2 + 8
if isinstance(c, str):
display.text_center(
tx, ty, c, s["text-style"], s["fg-color"], s["bg-color"]
)
else:
display.icon(tx - ICON // 2, ty - ICON, c, s["fg-color"], s["bg-color"])
def touch(self, event, pos):
pos = rotate(pos)
state = self.state
if state == BTN_DISABLED:
t = self.content
if isinstance(t, str):
display.text_center(tx, ty, t, s.text_style, s.fg_color, s.bg_color)
elif isinstance(t, bytes):
display.icon(tx - _ICON // 2, ty - _ICON, t, s.fg_color, s.bg_color)
def on_touch_start(self, x, y):
if self.state is _DISABLED:
return
if in_area(self.area, x, y):
self.state = _PRESSED
self.repaint = True
self.on_press_start()
if event == io.TOUCH_START:
if contains(self.area, pos):
self.state = BTN_ACTIVE
self.tainted = True
def on_touch_move(self, x, y):
if self.state is _DISABLED:
return
if in_area(self.area, x, y):
if self.state is _RELEASED:
self.state = _PRESSED
self.repaint = True
self.on_press_start()
else:
if self.state is _PRESSED:
self.state = _RELEASED
self.repaint = True
self.on_press_end()
elif event == io.TOUCH_MOVE:
if contains(self.area, pos):
if state == BTN_FOCUSED:
self.state = BTN_ACTIVE
self.tainted = True
else:
if state == BTN_ACTIVE:
self.state = BTN_FOCUSED
self.tainted = True
elif event == io.TOUCH_END:
if state != BTN_INITIAL:
self.state = BTN_INITIAL
self.tainted = True
if state == BTN_ACTIVE and contains(self.area, pos):
return BTN_CLICKED
def on_touch_end(self, x, y):
state = self.state
if state is not _INITIAL and state is not _DISABLED:
self.state = _INITIAL
self.repaint = True
if in_area(self.area, x, y):
if state is _PRESSED:
self.on_press_end()
self.on_click()
def on_press_start(self):
pass
def on_press_end(self):
pass
def on_click(self):
pass

@ -1,115 +1,106 @@
from micropython import const
from trezor import res, ui
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm
from trezor.ui.loader import Loader, LoaderDefault
from trezor import loop, res, ui
from trezor.ui import Widget
from trezor.ui.button import BTN_ACTIVE, BTN_CLICKED, Button
from trezor.ui.loader import Loader
CONFIRMED = object()
CANCELLED = object()
if __debug__:
from apps.debug import confirm_signal
CONFIRMED = const(1)
CANCELLED = const(2)
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
class Confirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
DEFAULT_CANCEL_STYLE = ButtonCancel
class ConfirmDialog(Widget):
def __init__(
self,
content,
confirm=DEFAULT_CONFIRM,
confirm_style=DEFAULT_CONFIRM_STYLE,
cancel=DEFAULT_CANCEL,
confirm_style=ui.BTN_CONFIRM,
cancel_style=ui.BTN_CANCEL,
cancel_style=DEFAULT_CANCEL_STYLE,
):
self.content = content
if confirm is not None:
if cancel is None:
area = ui.grid(4, n_x=1)
else:
area = ui.grid(9, n_x=2)
self.confirm = Button(area, confirm, confirm_style)
self.confirm.on_click = self.on_confirm
else:
self.confirm = None
if cancel is not None:
self.confirm = Button(ui.grid(9, n_x=2), confirm, style=confirm_style)
self.cancel = Button(ui.grid(8, n_x=2), cancel, style=cancel_style)
if confirm is None:
area = ui.grid(4, n_x=1)
else:
area = ui.grid(8, n_x=2)
self.cancel = Button(area, cancel, cancel_style)
self.cancel.on_click = self.on_cancel
else:
self.confirm = Button(ui.grid(4, n_x=1), confirm, style=confirm_style)
self.cancel = None
def render(self):
self.confirm.render()
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
if self.confirm is not None:
self.confirm.dispatch(event, x, y)
if self.cancel is not None:
self.cancel.render()
self.cancel.dispatch(event, x, y)
def touch(self, event, pos):
if self.confirm.touch(event, pos) == BTN_CLICKED:
return CONFIRMED
if self.cancel is not None:
if self.cancel.touch(event, pos) == BTN_CLICKED:
return CANCELLED
async def __iter__(self):
if __debug__:
return await loop.spawn(super().__iter__(), self.content, confirm_signal)
else:
return await loop.spawn(super().__iter__(), self.content)
def on_confirm(self):
raise ui.Result(CONFIRMED)
def on_cancel(self):
raise ui.Result(CANCELLED)
_STARTED = const(-1)
_STOPPED = const(-2)
class HoldToConfirm(ui.Layout):
DEFAULT_CONFIRM = "Hold To Confirm"
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_LOADER_STYLE = LoaderDefault
class HoldToConfirmDialog(Widget):
def __init__(
self,
content,
hold="Hold to confirm",
button_style=ui.BTN_CONFIRM,
loader_style=ui.LDR_DEFAULT,
confirm=DEFAULT_CONFIRM,
confirm_style=DEFAULT_CONFIRM_STYLE,
loader_style=DEFAULT_LOADER_STYLE,
):
self.content = content
self.button = Button(ui.grid(4, n_x=1), hold, style=button_style)
self.loader = Loader(style=loader_style)
if content.__class__.__iter__ is not Widget.__iter__:
raise TypeError(
"HoldToConfirmDialog does not support widgets with custom event loop"
)
def taint(self):
super().taint()
self.button.taint()
self.content.taint()
def render(self):
self.button.render()
if not self.loader.is_active():
self.content.render()
def touch(self, event, pos):
button = self.button
was_active = button.state == BTN_ACTIVE
button.touch(event, pos)
is_active = button.state == BTN_ACTIVE
if is_active and not was_active:
ui.display.clear()
self.loader.start()
return _STARTED
if was_active and not is_active:
if self.loader.stop():
return CONFIRMED
else:
return _STOPPED
async def __iter__(self):
result = None
while result is None or result < 0: # _STARTED or _STOPPED
if self.loader.is_active():
if __debug__:
result = await loop.spawn(
self.loader, super().__iter__(), confirm_signal
)
else:
result = await loop.spawn(self.loader, super().__iter__())
else:
self.content.taint()
if __debug__:
result = await loop.spawn(super().__iter__(), confirm_signal)
else:
result = await super().__iter__()
return result
self.loader = Loader(loader_style)
self.loader.on_start = self._on_loader_start
self.button = Button(ui.grid(4, n_x=1), confirm, confirm_style)
self.button.on_press_start = self._on_press_start
self.button.on_press_end = self._on_press_end
self.button.on_click = self._on_click
def _on_press_start(self):
self.loader.start()
def _on_press_end(self):
self.loader.stop()
def _on_loader_start(self):
# Loader has either started growing, or returned to the 0-position.
# In the first case we need to clear the content leftovers, in the latter
# we need to render the content again.
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT - 60, ui.BG)
self.content.dispatch(ui.REPAINT, 0, 0)
def _on_click(self):
if self.loader.elapsed_ms() >= self.loader.target_ms:
self.on_confirm()
def dispatch(self, event, x, y):
if self.loader.start_ms is not None:
self.loader.dispatch(event, x, y)
else:
self.content.dispatch(event, x, y)
self.button.dispatch(event, x, y)
def on_confirm(self):
raise ui.Result(CONFIRMED)

@ -1,21 +1,10 @@
from trezor.ui import Widget
from trezor import ui
class Container(Widget):
class Container(ui.Control):
def __init__(self, *children):
self.children = children
def taint(self):
super().taint()
def dispatch(self, event, x, y):
for child in self.children:
child.taint()
def render(self):
for child in self.children:
child.render()
def touch(self, event, pos):
for child in self.children:
result = child.touch(event, pos)
if result is not None:
return result
child.dispatch(event, x, y)

@ -1,33 +0,0 @@
from micropython import const
from trezor import loop, ui
from trezor.ui import Widget
from trezor.ui.button import BTN_CLICKED, Button
DEVICE = const(0)
HOST = const(1)
class EntrySelector(Widget):
def __init__(self, content):
self.content = content
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
def taint(self):
super().taint()
self.device.taint()
self.host.taint()
def render(self):
self.device.render()
self.host.render()
def touch(self, event, pos):
if self.device.touch(event, pos) == BTN_CLICKED:
return DEVICE
if self.host.touch(event, pos) == BTN_CLICKED:
return HOST
async def __iter__(self):
return await loop.spawn(super().__iter__(), self.content)

@ -1,36 +1,57 @@
import utime
from micropython import const
from trezor import loop, res, ui
from trezor import res, ui
from trezor.ui import display
class LoaderDefault:
class normal:
bg_color = ui.BG
fg_color = ui.GREEN
icon = None
icon_fg_color = None
class active:
bg_color = ui.BG
fg_color = ui.GREEN
icon = ui.ICON_CHECK
icon_fg_color = ui.WHITE
class LoaderDanger:
class normal(LoaderDefault.normal):
fg_color = ui.RED
class active(LoaderDefault.active):
fg_color = ui.RED
_TARGET_MS = const(1000)
_SHRINK_BY = const(2)
class Loader(ui.Widget):
def __init__(self, style=ui.LDR_DEFAULT):
class Loader(ui.Control):
def __init__(self, style=LoaderDefault):
self.normal_style = style.normal
self.active_style = style.active
self.target_ms = _TARGET_MS
self.normal_style = style["normal"] or ui.LDR_DEFAULT["normal"]
self.active_style = style["active"] or ui.LDR_DEFAULT["active"]
self.start_ms = None
self.stop_ms = None
def start(self):
self.start_ms = utime.ticks_ms()
self.stop_ms = None
self.on_start()
def stop(self):
if self.start_ms is not None and self.stop_ms is None:
diff_ms = utime.ticks_ms() - self.start_ms
else:
diff_ms = 0
self.stop_ms = utime.ticks_ms()
return diff_ms >= self.target_ms
def is_active(self):
return self.start_ms is not None
def elapsed_ms(self):
if self.start_ms is None:
return 0
return utime.ticks_ms() - self.start_ms
def render(self):
def on_render(self):
target = self.target_ms
start = self.start_ms
stop = self.stop_ms
@ -38,35 +59,24 @@ class Loader(ui.Widget):
if stop is None:
r = min(now - start, target)
else:
r = max(stop - start + (stop - now) * _SHRINK_BY, 0)
if r == 0:
self.start_ms = None
self.stop_ms = None
r = max(stop - start + (stop - now) * 2, 0)
if r == target:
s = self.active_style
else:
s = self.normal_style
if s["icon"] is None:
ui.display.loader(r, False, -24, s["fg-color"], s["bg-color"])
elif s["icon-fg-color"] is None:
ui.display.loader(
r, False, -24, s["fg-color"], s["bg-color"], res.load(s["icon"])
)
Y = const(-24)
if s.icon is None:
display.loader(r, False, Y, s.fg_color, s.bg_color)
else:
ui.display.loader(
r,
False,
-24,
s["fg-color"],
s["bg-color"],
res.load(s["icon"]),
s["icon-fg-color"],
display.loader(
r, False, Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color
)
if r == 0:
self.start_ms = None
self.stop_ms = None
self.on_start()
def __iter__(self):
sleep = loop.sleep(1000000 // 30) # 30 fps
ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear
while self.is_active():
self.render()
yield sleep
ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear
def on_start(self):
pass

@ -1,16 +1,7 @@
from trezor import io, loop, res, ui
from trezor.crypto import bip39
from trezor.ui import display
from trezor.ui.button import BTN_CLICKED, ICON, Button
if __debug__:
from apps.debug import input_signal
MNEMONIC_KEYS = ("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz")
def key_buttons(keys):
return [Button(ui.grid(i + 3, n_y=4), k) for i, k in enumerate(keys)]
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
def compute_mask(text: str) -> int:
@ -23,42 +14,59 @@ def compute_mask(text: str) -> int:
return mask
class Input(Button):
def __init__(self, area: tuple, content: str = "", word: str = ""):
class KeyButton(Button):
def __init__(self, area, content, keyboard):
self.keyboard = keyboard
super().__init__(area, content)
def on_click(self):
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area, content, word):
super().__init__(area, content)
self.word = word
self.icon = None
self.pending = False
self.icon = None
self.disable()
def edit(self, content: str, word: str, pending: bool):
def edit(self, content, word, pending):
self.word = word
self.content = content
self.pending = pending
self.taint()
if content == word: # confirm button
self.enable()
self.normal_style = ui.BTN_KEY_CONFIRM["normal"]
self.active_style = ui.BTN_KEY_CONFIRM["active"]
self.icon = ui.ICON_CONFIRM
elif word: # auto-complete button
self.enable()
self.normal_style = ui.BTN_KEY["normal"]
self.active_style = ui.BTN_KEY["active"]
self.icon = ui.ICON_CLICK
self.repaint = True
if word:
if content == word: # confirm button
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = ui.ICON_CONFIRM
else: # auto-complete button
self.enable()
self.normal_style = ButtonMono.normal
self.active_style = ButtonMono.active
self.icon = ui.ICON_CLICK
else: # disabled button
self.disabled_style = ButtonMono.disabled
self.disable()
self.icon = None
def render_content(self, s, ax, ay, aw, ah):
text_style = s["text-style"]
fg_color = s["fg-color"]
bg_color = s["bg-color"]
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
p = self.pending # should we draw the pending marker?
t = self.content # input content
w = self.word[len(t) :] # suggested word
i = self.icon # rendered icon
if not t:
# render prompt
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
return
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
@ -73,79 +81,103 @@ class Input(Button):
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if i: # icon
ix = ax + aw - ICON * 2
iy = ty - ICON
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, res.load(i), fg_color, bg_color)
class MnemonicKeyboard(ui.Widget):
def __init__(self, prompt: str = ""):
class Prompt(ui.Control):
def __init__(self, prompt):
self.prompt = prompt
self.input = Input(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
self.back = Button(
ui.grid(0, n_x=4, n_y=4), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR
)
self.keys = key_buttons(MNEMONIC_KEYS)
self.pbutton = None # pending key button
self.pindex = 0 # index of current pending char in pbutton
def taint(self):
super().taint()
self.input.taint()
self.back.taint()
for btn in self.keys:
btn.taint()
self.repaint = True
def render(self):
if self.input.content:
# content button and backspace
self.input.render()
self.back.render()
else:
# prompt
def on_render(self):
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
# key buttons
self.repaint = False
class MnemonicKeyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self)
for i, k in enumerate(
("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz")
)
]
self.pending_button = None
self.pending_index = 0
def dispatch(self, event: int, x: int, y: int):
for btn in self.keys:
btn.render()
btn.dispatch(event, x, y)
if self.input.content:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self):
# Backspace was clicked, let's delete the last character of input.
self.edit(self.input.content[:-1])
def touch(self, event, pos):
def on_input_click(self):
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
content = self.input.content
word = self.input.word
if word and word == content:
self.edit("")
self.on_confirm(word)
else:
self.edit(word)
def on_key_click(self, btn: Button):
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.content)
content = self.input.content[:-1] + btn.content[index]
else:
index = 0
content = self.input.content + btn.content[0]
self.edit(content, btn, index)
def on_timeout(self):
# Timeout occurred. If we can auto-complete current input, let's just
# reset the pending marker. If not, input is invalid, let's backspace
# the last character.
if self.input.word:
self.edit(self.input.content)
else:
self.edit(self.input.content[:-1])
if self.back.touch(event, pos) == BTN_CLICKED:
# backspace, delete the last character of input
self.edit(content[:-1])
return
def on_confirm(self, word):
# Word was confirmed by the user.
raise ui.Result(word)
if self.input.touch(event, pos) == BTN_CLICKED:
# input press, either auto-complete or confirm
if word and content == word:
self.edit("")
return content
else:
self.edit(word)
return
def edit(self, content: str, button: KeyButton = None, index: int = 0):
self.pending_button = button
self.pending_index = index
for btn in self.keys:
if btn.touch(event, pos) == BTN_CLICKED:
# key press, add new char to input or cycle the pending button
if self.pbutton is btn:
index = (self.pindex + 1) % len(btn.content)
content = content[:-1] + btn.content[index]
else:
index = 0
content += btn.content[0]
self.edit(content, btn, index)
return
def edit(self, content, button=None, index=0):
# find the completions
pending = button is not None
word = bip39.find_word(content) or ""
mask = bip39.complete_word(content)
self.pbutton = button
self.pindex = index
self.input.edit(content, word, button is not None)
# modify the input state
self.input.edit(content, word, pending)
# enable or disable key buttons
for btn in self.keys:
@ -154,37 +186,25 @@ class MnemonicKeyboard(ui.Widget):
else:
btn.disable()
async def __iter__(self):
if __debug__:
return await loop.spawn(self.edit_loop(), input_signal)
else:
return await self.edit_loop()
# invalidate the prompt if we display it next frame
if not self.input.content:
self.prompt.repaint = True
async def edit_loop(self):
timeout = loop.sleep(1000 * 1000 * 1)
async def handle_input(self):
touch = loop.wait(io.TOUCH)
wait_timeout = loop.spawn(touch, timeout)
wait_touch = loop.spawn(touch)
content = None
self.back.taint()
self.input.taint()
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while content is None:
self.render()
if self.pbutton is not None:
wait = wait_timeout
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
wait = wait_touch
result = await wait
if touch in wait.finished:
event, *pos = result
content = self.touch(event, pos)
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
if self.input.word:
# just reset the pending state
self.edit(self.input.content)
else:
# invalid character, backspace it
self.edit(self.input.content[:-1])
return content
self.on_timeout()

@ -1,17 +1,14 @@
from trezor import res, ui
from trezor.ui import display
from trezor.ui.button import BTN_CLICKED, Button
ITEMS_PER_PAGE = 10
PLUS_BUTTON_POSITION = 11
BACK_BUTTON_POSITION = 9
from micropython import const
from trezor import res, ui
from trezor.ui.button import Button, ButtonMono, ButtonMonoDark
def digit_area(i):
return ui.grid(i + 3) # skip the first line
_ITEMS_PER_PAGE = const(10)
_BACK_BUTTON_POSITION = const(9)
_PLUS_BUTTON_POSITION = const(11)
class NumPad(ui.Widget):
class NumPad(ui.Layout):
def __init__(self, label: str, start: int, end: int):
"""
Generates a numpad with numbers from `start` to `end` excluding.
@ -20,50 +17,63 @@ class NumPad(ui.Widget):
self.start = start
self.end = end
self.page = 0
self.buttons = generate_buttons(self.start, self.end, self.page, self)
self._generate_buttons()
def render(self):
for btn in self.buttons:
btn.render()
# header label
display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG)
def touch(self, event, pos):
for btn in self.buttons:
if btn.touch(event, pos) == BTN_CLICKED:
if "+" in btn.content:
self.page += 1
self._generate_buttons()
elif isinstance(btn.content, bytes):
self.page -= 1
self._generate_buttons()
else:
return btn.content
def _generate_buttons(self):
display.clear() # we need to clear old buttons
start = self.start + (ITEMS_PER_PAGE + 1) * self.page - self.page
end = min(self.end, (ITEMS_PER_PAGE + 1) * (self.page + 1) - self.page)
digits = list(range(start, end))
self.buttons = [Button(digit_area(i), str(d)) for i, d in enumerate(digits)]
if len(digits) == ITEMS_PER_PAGE:
more = Button(
digit_area(PLUS_BUTTON_POSITION), str(end) + "+", style=ui.BTN_KEY_DARK
)
self.buttons.append(more)
# move the tenth button to its proper place and make place for the back button
self.buttons[BACK_BUTTON_POSITION].area = digit_area(
BACK_BUTTON_POSITION + 1
def dispatch(self, event, x, y):
for button in self.buttons:
button.dispatch(event, x, y)
if event is ui.RENDER:
# render header label
ui.display.text_center(
ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG
)
back = Button(
digit_area(BACK_BUTTON_POSITION),
res.load(ui.ICON_BACK),
style=ui.BTN_KEY_DARK,
)
if self.page == 0:
back.disable()
self.buttons.append(back)
def on_back(self):
self.page -= 1
self.buttons = generate_buttons(self.start, self.end, self.page, self)
ui.display.clear() # we need to clear old buttons
def on_plus(self):
self.page += 1
self.buttons = generate_buttons(self.start, self.end, self.page, self)
ui.display.clear() # we need to clear old buttons
def on_select(self, number):
raise ui.Result(number)
class NumButton(Button):
def __init__(self, index, digit, pad):
self.pad = pad
area = ui.grid(index + 3) # skip the first line
super().__init__(area, str(digit), ButtonMono)
def on_click(self):
self.pad.on_select(int(self.content))
def generate_buttons(start, end, page, pad):
start = start + (_ITEMS_PER_PAGE + 1) * page - page
end = min(end, (_ITEMS_PER_PAGE + 1) * (page + 1) - page)
digits = list(range(start, end))
buttons = [NumButton(i, d, pad) for i, d in enumerate(digits)]
area = ui.grid(_PLUS_BUTTON_POSITION + 3)
plus = Button(area, str(end) + "+", ButtonMonoDark)
plus.on_click = pad.on_plus
area = ui.grid(_BACK_BUTTON_POSITION + 3)
back = Button(area, res.load(ui.ICON_BACK), ButtonMonoDark)
back.on_click = pad.on_back
if len(digits) == _ITEMS_PER_PAGE:
# move the tenth button to its proper place and make place for the back button
buttons[-1].area = ui.grid(_PLUS_BUTTON_POSITION - 1 + 3)
buttons.append(plus)
if page == 0:
back.disable()
buttons.append(back)
return buttons

@ -1,8 +1,9 @@
from micropython import const
from trezor import io, loop, res, ui
from trezor.messages import PassphraseSourceType
from trezor.ui import display
from trezor.ui.button import BTN_CLICKED, Button
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
SPACE = res.load(ui.ICON_SPACE)
@ -21,45 +22,60 @@ def digit_area(i):
return ui.grid(i + 3) # skip the first line
def key_buttons(keys):
return [Button(digit_area(i), k) for i, k in enumerate(keys)]
def render_scrollbar(page):
bbox = const(240)
size = const(8)
BBOX = const(240)
SIZE = const(8)
pages = len(KEYBOARD_KEYS)
padding = 12
page_count = len(KEYBOARD_KEYS)
if pages * padding > BBOX:
padding = BBOX // pages
x = (BBOX // 2) - (pages // 2) * padding
Y = const(44)
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
fg = ui.DARK_GREY
ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2)
if page_count * padding > bbox:
padding = bbox // page_count
x = (bbox // 2) - (page_count // 2) * padding
y = 44
class KeyButton(Button):
def __init__(self, area, content, keyboard):
self.keyboard = keyboard
super().__init__(area, content)
def on_click(self):
self.keyboard.on_key_click(self)
def get_text_content(self):
if self.content is SPACE:
return " "
else:
return self.content
for i in range(0, page_count):
if i != page:
ui.display.bar_radius(
x + i * padding, y, size, size, ui.DARK_GREY, ui.BG, size // 2
)
ui.display.bar_radius(x + page * padding, y, size, size, ui.FG, ui.BG, size // 2)
def key_buttons(keys, keyboard):
return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)]
class Input(Button):
def __init__(self, area: tuple, content: str = ""):
def __init__(self, area, content):
super().__init__(area, content)
self.pending = False
self.disable()
def edit(self, content: str, pending: bool):
def edit(self, content, pending):
self.content = content
self.pending = pending
self.taint()
self.repaint = True
def render_content(self, s, ax, ay, aw, ah):
text_style = s["text-style"]
fg_color = s["fg-color"]
bg_color = s["bg-color"]
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
p = self.pending # should we draw the pending marker?
t = self.content # input content
@ -76,142 +92,167 @@ class Input(Button):
if p: # pending marker
pw = display.text_width(t[-1:], text_style)
display.bar(tx + width - pw, ty + 2, pw + 1, 3, fg_color)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
else: # cursor
display.bar(tx + width + 1, ty - 18, 2, 22, fg_color)
cx = tx + width + 1
display.bar(cx, ty - 18, 2, 22, fg_color)
def on_click(self):
pass
class Prompt(ui.Widget):
class Prompt(ui.Control):
def __init__(self, text):
self.text = text
self.repaint = True
def render(self):
if self.tainted:
def on_render(self):
if self.repaint:
display.bar(0, 0, ui.WIDTH, 48, ui.BG)
display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG)
self.tainted = False
self.repaint = False
CANCELLED = const(0)
CANCELLED = object()
class PassphraseKeyboard(ui.Widget):
def __init__(self, prompt, page=1):
class PassphraseKeyboard(ui.Layout):
def __init__(self, prompt, max_length, page=1):
self.prompt = Prompt(prompt)
self.max_length = max_length
self.page = page
self.input = Input(ui.grid(0, n_x=1, n_y=6), "")
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR)
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), style=ui.BTN_CONFIRM)
self.keys = key_buttons(KEYBOARD_KEYS[self.page])
self.pbutton = None # pending key button
self.pindex = 0 # index of current pending char in pbutton
def taint(self):
super().taint()
self.prompt.taint()
self.input.taint()
self.back.taint()
self.done.taint()
for btn in self.keys:
btn.taint()
def render(self):
# passphrase or prompt
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear)
self.back.on_click = self.on_back_click
self.back.disable()
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm)
self.done.on_click = self.on_confirm
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.pending_button = None
self.pending_index = 0
def dispatch(self, event, x, y):
if self.input.content:
self.input.render()
self.input.dispatch(event, x, y)
else:
self.prompt.render()
render_scrollbar(self.page)
# buttons
self.back.render()
self.done.render()
self.prompt.dispatch(event, x, y)
self.back.dispatch(event, x, y)
self.done.dispatch(event, x, y)
for btn in self.keys:
btn.render()
btn.dispatch(event, x, y)
if event == ui.RENDER:
render_scrollbar(self.page)
def touch(self, event, pos):
def on_back_click(self):
# Backspace was clicked. If we have any content in the input, let's delete
# the last character. Otherwise cancel.
content = self.input.content
if self.back.touch(event, pos) == BTN_CLICKED:
if content:
# backspace, delete the last character of input
self.edit(content[:-1])
return
else:
# cancel
return CANCELLED
if self.done.touch(event, pos) == BTN_CLICKED:
# confirm button, return the content
return content
for btn in self.keys:
if btn.touch(event, pos) == BTN_CLICKED:
if isinstance(btn.content[0], str):
# key press, add new char to input or cycle the pending button
if self.pbutton is btn:
index = (self.pindex + 1) % len(btn.content)
content = content[:-1] + btn.content[index]
else:
index = 0
content += btn.content[0]
else:
index = 0
content += " "
self.edit(content, btn, index)
return
def edit(self, content, button=None, index=0):
if button and len(button.content) == 1:
# one-letter buttons are never pending
button = None
if content:
self.edit(content[:-1])
else:
self.on_cancel()
def on_key_click(self, button: KeyButton):
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
button_text = button.get_text_content()
if self.pending_button is button:
index = (self.pending_index + 1) % len(button_text)
prefix = self.input.content[:-1]
else:
index = 0
self.pbutton = button
self.pindex = index
self.input.edit(content, button is not None)
prefix = self.input.content
if len(button_text) > 1:
self.edit(prefix + button_text[index], button, index)
else:
self.edit(prefix + button_text[index])
def on_timeout(self):
# Timeout occurred, let's just reset the pending marker.
self.edit(self.input.content)
def edit(self, content: str, button: Button = None, index: int = 0):
if len(content) > self.max_length:
return
self.pending_button = button
self.pending_index = index
# modify the input state
pending = button is not None
self.input.edit(content, pending)
if content:
self.back.enable()
else:
self.back.disable()
self.prompt.taint()
self.prompt.repaint = True
async def __iter__(self):
self.edit(self.input.content) # init button state
while True:
change = self.change_page()
enter = self.enter_text()
wait = loop.spawn(change, enter)
result = await wait
if enter in wait.finished:
return result
@ui.layout
async def enter_text(self):
timeout = loop.sleep(1000 * 1000 * 1)
async def handle_input(self):
touch = loop.wait(io.TOUCH)
wait_timeout = loop.spawn(touch, timeout)
wait_touch = loop.spawn(touch)
content = None
while content is None:
self.render()
if self.pbutton is not None:
wait = wait_timeout
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
wait = wait_touch
result = await wait
if touch in wait.finished:
event, *pos = result
content = self.touch(event, pos)
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
# disable the pending buttons
self.edit(self.input.content)
return content
self.on_timeout()
async def change_page(self):
swipe = await Swipe(directions=SWIPE_HORIZONTAL)
async def handle_paging(self):
swipe = await Swipe(SWIPE_HORIZONTAL)
if swipe == SWIPE_LEFT:
self.page = (self.page + 1) % len(KEYBOARD_KEYS)
else:
self.page = (self.page - 1) % len(KEYBOARD_KEYS)
self.keys = key_buttons(KEYBOARD_KEYS[self.page])
self.back.taint()
self.done.taint()
self.input.taint()
self.prompt.taint()
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.back.repaint = True
self.done.repaint = True
self.input.repaint = True
self.prompt.repaint = True
def on_cancel(self):
raise ui.Result(CANCELLED)
def on_confirm(self):
raise ui.Result(self.input.content)
def create_tasks(self):
return self.handle_input(), self.handle_rendering(), self.handle_paging()
class PassphraseSource(ui.Layout):
def __init__(self, content):
self.content = content
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
self.device.on_click = self.on_device
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
self.host.on_click = self.on_host
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
self.device.dispatch(event, x, y)
self.host.dispatch(event, x, y)
def on_device(self):
raise ui.Result(PassphraseSourceType.DEVICE)
def on_host(self):
raise ui.Result(PassphraseSourceType.HOST)

@ -1,9 +1,15 @@
from micropython import const
from trezor import ui
from trezor import res, ui
from trezor.crypto import random
from trezor.ui import display
from trezor.ui.button import BTN_CLICKED, Button
from trezor.ui.button import (
Button,
ButtonCancel,
ButtonClear,
ButtonConfirm,
ButtonMono,
)
def digit_area(i):
@ -15,78 +21,119 @@ def digit_area(i):
def generate_digits():
digits = list(range(0, 10)) # 0-9
random.shuffle(digits)
return digits
# We lay out the buttons top-left to bottom-right, but the order
# of the digits is defined as bottom-left to top-right (on numpad).
return digits[6:] + digits[3:6] + digits[:3]
class PinMatrix(ui.Widget):
def __init__(self, label, sublabel, pin="", maxlength=9):
self.label = label
self.sublabel = sublabel
class PinInput(ui.Control):
def __init__(self, prompt, subprompt, pin):
self.prompt = prompt
self.subprompt = subprompt
self.pin = pin
self.maxlength = maxlength
self.digits = generate_digits()
self.repaint = True
# we lay out the buttons top-left to bottom-right, but the order of the
# digits is defined as bottom-left to top-right (on numpad)
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
def on_render(self):
if self.repaint:
if self.pin:
self.render_pin()
else:
self.render_prompt()
self.repaint = False
def render_pin(self):
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
count = len(self.pin)
BOX_WIDTH = const(240)
DOT_SIZE = const(10)
PADDING = const(14)
RENDER_Y = const(20)
render_x = (BOX_WIDTH - count * PADDING) // 2
for i in range(0, count):
display.bar_radius(
render_x + i * PADDING, RENDER_Y, DOT_SIZE, DOT_SIZE, ui.GREY, ui.BG, 4
)
self.pin_buttons = [
Button(digit_area(i), str(d)) for i, d in enumerate(reordered_digits)
]
self.onchange = None
def render_prompt(self):
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
if self.subprompt:
display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG)
display.text_center(
ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG
)
else:
display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG)
def taint(self):
super().taint()
for btn in self.pin_buttons:
btn.taint()
def render(self):
# pin matrix buttons
for btn in self.pin_buttons:
btn.render()
class PinButton(Button):
def __init__(self, index, digit, matrix):
self.matrix = matrix
super().__init__(digit_area(index), str(digit), ButtonMono)
if not self.tainted:
return
def on_click(self):
self.matrix.assign(self.matrix.input.pin + self.content)
# clear canvas under input line
display.bar(0, 0, ui.WIDTH, 52, ui.BG)
if self.pin:
# input line with pin
l = len(self.pin)
y = const(20)
size = const(10)
padding = const(14)
box_w = const(240)
x = (box_w - l * padding) // 2
for i in range(0, l):
ui.display.bar_radius(x + i * padding, y, size, size, ui.GREY, ui.BG, 4)
elif self.sublabel:
# input line with header label and sublabel
display.text_center(ui.WIDTH // 2, 20, self.label, ui.BOLD, ui.GREY, ui.BG)
display.text_center(
ui.WIDTH // 2, 46, self.sublabel, ui.NORMAL, ui.GREY, ui.BG
)
CANCELLED = object()
class PinDialog(ui.Layout):
def __init__(self, prompt, subprompt, allow_cancel=True, maxlength=9):
self.maxlength = maxlength
self.input = PinInput(prompt, subprompt, "")
icon_confirm = res.load(ui.ICON_CONFIRM)
self.confirm_button = Button(ui.grid(14), icon_confirm, ButtonConfirm)
self.confirm_button.on_click = self.on_confirm
icon_back = res.load(ui.ICON_BACK)
self.reset_button = Button(ui.grid(12), icon_back, ButtonClear)
self.reset_button.on_click = self.on_reset
if allow_cancel:
icon_lock = res.load(ui.ICON_LOCK)
self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel)
self.cancel_button.on_click = self.on_cancel
else:
# input line with header label
display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG)
self.cancel_button = Button(ui.grid(12), "")
self.cancel_button.disable()
self.tainted = False
self.pin_buttons = [
PinButton(i, d, self) for i, d in enumerate(generate_digits())
]
def touch(self, event, pos):
def dispatch(self, event, x, y):
for btn in self.pin_buttons:
if btn.touch(event, pos) == BTN_CLICKED:
if len(self.pin) < self.maxlength:
self.change(self.pin + btn.content)
break
btn.dispatch(event, x, y)
self.input.dispatch(event, x, y)
self.confirm_button.dispatch(event, x, y)
if self.input.pin:
self.reset_button.dispatch(event, x, y)
else:
self.cancel_button.dispatch(event, x, y)
def change(self, pin):
self.tainted = True
self.pin = pin
def assign(self, pin):
if len(pin) > self.maxlength:
return
for btn in self.pin_buttons:
if len(self.pin) == self.maxlength:
btn.disable()
else:
if len(pin) < self.maxlength:
btn.enable()
if self.onchange:
self.onchange()
else:
btn.disable()
if pin:
self.reset_button.enable()
self.cancel_button.disable()
else:
self.reset_button.disable()
self.cancel_button.enable()
self.input.pin = pin
self.input.repaint = True
def on_reset(self):
self.assign("")
def on_cancel(self):
raise ui.Result(CANCELLED)
def on_confirm(self):
raise ui.Result(self.input.pin)

@ -0,0 +1,17 @@
from trezor import loop, ui
class Popup(ui.Layout):
def __init__(self, content, time_ms=0):
self.content = content
self.time_ms = time_ms
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
def create_tasks(self):
return self.handle_input(), self.handle_rendering(), self.handle_timeout()
def handle_timeout(self):
yield loop.sleep(self.time_ms * 1000)
raise ui.Result(None)

@ -1,11 +1,12 @@
from trezor import ui
class Qr(ui.Widget):
def __init__(self, data, pos, scale):
class Qr(ui.Control):
def __init__(self, data, x, y, scale):
self.data = data
self.pos = pos
self.x = x
self.y = y
self.scale = scale
def render(self):
ui.display.qrcode(self.pos[0], self.pos[1], self.data, self.scale)
def on_render(self):
ui.display.qrcode(self.x, self.y, self.data, self.scale)

@ -1,92 +1,180 @@
from micropython import const
from trezor import loop, res, ui
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
from trezor.ui.confirm import CANCELLED, CONFIRMED
from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
if __debug__:
from apps.debug import swipe_signal
async def change_page(page, page_count):
while True:
if page == 0:
d = SWIPE_UP
elif page == page_count - 1:
d = SWIPE_DOWN
else:
d = SWIPE_VERTICAL
swipe = Swipe(directions=d)
if __debug__:
s = await loop.spawn(swipe, swipe_signal)
else:
s = await swipe
if s == SWIPE_UP:
return min(page + 1, page_count - 1) # scroll down
elif s == SWIPE_DOWN:
return max(page - 1, 0) # scroll up
async def paginate(render_page, page_count, page=0, *args):
while True:
changer = change_page(page, page_count)
renderer = render_page(page, page_count, *args)
waiter = loop.spawn(changer, renderer)
result = await waiter
if changer in waiter.finished:
page = result
def render_scrollbar(pages: int, page: int):
BBOX = const(220)
SIZE = const(8)
padding = 14
if pages * padding > BBOX:
padding = BBOX // pages
X = const(220)
Y = (BBOX // 2) - (pages // 2) * padding
for i in range(0, pages):
if i == page:
fg = ui.FG
else:
return result
fg = ui.GREY
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
def render_swipe_icon():
DRAW_DELAY = const(200000)
icon = res.load(ui.ICON_SWIPE)
t = ui.pulse(DRAW_DELAY)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
ui.display.icon(70, 205, icon, c, ui.BG)
async def animate_swipe():
time_delay = const(40000)
draw_delay = const(200000)
def render_swipe_text():
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
sleep = loop.sleep(time_delay)
icon = res.load(ui.ICON_SWIPE)
for t in ui.pulse(draw_delay):
fg = ui.blend(ui.GREY, ui.DARK_GREY, t)
ui.display.icon(70, 205, icon, fg, ui.BG)
yield sleep
class Paginated(ui.Layout):
def __init__(self, pages, page=0, one_by_one=False):
self.pages = pages
self.page = page
self.one_by_one = one_by_one
self.repaint = True
def dispatch(self, event, x, y):
pages = self.pages
page = self.page
pages[page].dispatch(event, x, y)
if event is ui.RENDER:
length = len(pages)
if page < length - 1:
render_swipe_icon()
if self.repaint:
render_swipe_text()
if self.repaint:
render_scrollbar(length, page)
self.repaint = False
async def handle_paging(self):
if self.page == 0:
directions = SWIPE_UP
elif self.page == len(self.pages) - 1:
directions = SWIPE_DOWN
else:
directions = SWIPE_VERTICAL
if __debug__:
swipe = await loop.spawn(Swipe(directions), swipe_signal)
else:
swipe = await Swipe(directions)
def render_scrollbar(page, page_count):
bbox = const(220)
size = const(8)
if swipe is SWIPE_UP:
self.page = min(self.page + 1, len(self.pages) - 1)
elif swipe is SWIPE_DOWN:
self.page = max(self.page - 1, 0)
padding = 14
if page_count * padding > bbox:
padding = bbox // page_count
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.repaint = True
x = const(220)
y = (bbox // 2) - (page_count // 2) * padding
self.on_change()
for i in range(0, page_count):
if i != page:
ui.display.bar_radius(x, y + i * padding, size, size, ui.GREY, ui.BG, 4)
ui.display.bar_radius(x, y + page * padding, size, size, ui.FG, ui.BG, 4)
def create_tasks(self):
return self.handle_input(), self.handle_rendering(), self.handle_paging()
def on_change(self):
if self.one_by_one:
raise ui.Result(self.page)
class Scrollpage(ui.Widget):
def __init__(self, content, page, page_count):
self.content = content
self.page = page
self.page_count = page_count
if content.__class__.__iter__ is not ui.Widget.__iter__:
raise TypeError(
"Scrollpage does not support widgets with custom event loop"
)
class PageWithButtons(ui.Control):
def __init__(self, content, paginated, index, count):
self.content = content
self.paginated = paginated
self.index = index
self.count = count
if self.index == 0:
# first page, we can cancel or go down
left = res.load(ui.ICON_CANCEL)
left_style = ButtonCancel
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
elif self.index == count - 1:
# last page, we can go up or confirm
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CONFIRM)
right_style = ButtonConfirm
else:
# somewhere in the middle, we can go up or down
left = res.load(ui.ICON_BACK)
left_style = ButtonDefault
right = res.load(ui.ICON_CLICK)
right_style = ButtonDefault
self.left = Button(ui.grid(8, n_x=2), left, left_style)
self.left.on_click = self.on_left
self.right = Button(ui.grid(9, n_x=2), right, right_style)
self.right.on_click = self.on_right
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
self.left.dispatch(event, x, y)
self.right.dispatch(event, x, y)
def on_left(self):
if self.index == 0:
self.paginated.on_cancel()
else:
self.paginated.on_down()
def taint(self):
super().taint()
self.content.taint()
def on_right(self):
if self.index == self.count - 1:
self.paginated.on_confirm()
else:
self.paginated.on_up()
def render(self):
self.content.render()
render_scrollbar(self.page, self.page_count)
def touch(self, event, pos):
return self.content.touch(event, pos)
class PaginatedWithButtons(ui.Layout):
def __init__(self, pages, page=0, one_by_one=False):
self.pages = [
PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages)
]
self.page = page
self.one_by_one = one_by_one
def dispatch(self, event, x, y):
pages = self.pages
page = self.page
pages[page].dispatch(event, x, y)
if event is ui.RENDER:
render_scrollbar(len(pages), page)
def on_up(self):
self.page = max(self.page - 1, 0)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_down(self):
self.page = min(self.page + 1, len(self.pages) - 1)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.on_change()
def on_confirm(self):
raise ui.Result(CONFIRMED)
def on_cancel(self):
raise ui.Result(CANCELLED)
def on_change(self):
if self.one_by_one:
raise ui.Result(self.page)

@ -1,6 +1,6 @@
from micropython import const
from trezor.ui import BOLD, MONO, NORMAL, rgb
from trezor.ui import rgb
# radius for buttons and other elements
RADIUS = const(2)
@ -66,193 +66,3 @@ ICON_BACK = "trezor/res/left.toig"
ICON_SWIPE = "trezor/res/swipe.toig"
ICON_CHECK = "trezor/res/check.toig"
ICON_SPACE = "trezor/res/space.toig"
# buttons
BTN_DEFAULT = {
"normal": {
"bg-color": BG,
"fg-color": FG,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": BG,
"text-style": BOLD,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_CANCEL = {
"normal": {
"bg-color": RED,
"fg-color": FG,
"text-style": BOLD,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": RED,
"text-style": BOLD,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_CONFIRM = {
"normal": {
"bg-color": GREEN,
"fg-color": FG,
"text-style": BOLD,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": GREEN,
"text-style": BOLD,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_CLEAR = {
"normal": {
"bg-color": ORANGE,
"fg-color": FG,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": BG,
"fg-color": GREY,
"text-style": NORMAL,
"border-color": BG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_KEY = {
"normal": {
"bg-color": BLACKISH,
"fg-color": FG,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": BLACKISH,
"text-style": MONO,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_KEY_DARK = {
"normal": {
"bg-color": DARK_BLACK,
"fg-color": DARK_WHITE,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": DARK_BLACK,
"text-style": MONO,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": DARK_BLACK,
"fg-color": GREY,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
}
BTN_KEY_CONFIRM = {
"normal": {
"bg-color": GREEN,
"fg-color": FG,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
"active": {
"bg-color": FG,
"fg-color": GREEN,
"text-style": MONO,
"border-color": FG,
"radius": RADIUS,
},
"disabled": {
"bg-color": BG,
"fg-color": GREY,
"text-style": MONO,
"border-color": BG,
"radius": RADIUS,
},
}
# loader
LDR_DEFAULT = {
"normal": {"bg-color": BG, "fg-color": GREEN, "icon": None, "icon-fg-color": None},
"active": {
"bg-color": BG,
"fg-color": GREEN,
"icon": ICON_CHECK,
"icon-fg-color": WHITE,
},
}
LDR_DANGER = {
"normal": {"bg-color": BG, "fg-color": RED, "icon": None, "icon-fg-color": None},
"active": {
"bg-color": BG,
"fg-color": RED,
"icon": ICON_CHECK,
"icon-fg-color": WHITE,
},
}

@ -1,7 +1,6 @@
from micropython import const
from trezor import io, ui
from trezor.ui import contains, rotate
from trezor import io, loop, ui
SWIPE_UP = const(0x01)
SWIPE_DOWN = const(0x02)
@ -12,79 +11,99 @@ SWIPE_HORIZONTAL = const(SWIPE_LEFT | SWIPE_RIGHT)
SWIPE_ALL = const(SWIPE_VERTICAL | SWIPE_HORIZONTAL)
_SWIPE_DISTANCE = const(120)
_SWIPE_TRESHOLD = const(30)
class Swipe(ui.Widget):
def __init__(self, area=None, absolute=False, directions=SWIPE_ALL, treshold=30):
self.area = area or (0, 0, ui.WIDTH, ui.HEIGHT)
self.absolute = absolute
class Swipe(ui.Control):
def __init__(self, directions=SWIPE_ALL, area=None):
if area is None:
area = (0, 0, ui.WIDTH, ui.HEIGHT)
self.area = area
self.directions = directions
self.treshold = treshold
self.start_pos = None
self.start_x = None
self.start_y = None
self.light_origin = None
self.light_target = ui.BACKLIGHT_NONE
def touch(self, event, pos):
if not self.absolute:
pos = rotate(pos)
def on_touch_start(self, x, y):
if ui.in_area(self.area, x, y):
self.start_x = x
self.start_y = y
self.light_origin = ui.BACKLIGHT_NORMAL
if event == io.TOUCH_MOVE and self.start_pos is not None:
pdx = pos[0] - self.start_pos[0]
pdy = pos[1] - self.start_pos[1]
def on_touch_move(self, x, y):
if self.start_x is None:
return # not started in our area
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and self.directions & SWIPE_HORIZONTAL:
# Horizontal direction
if (pdx > 0 and self.directions & SWIPE_RIGHT) or (
pdx < 0 and self.directions & SWIPE_LEFT
):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1,
)
dirs = self.directions
pdx = x - self.start_x
pdy = y - self.start_y
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
# horizontal direction
if (pdx > 0 and dirs & SWIPE_RIGHT) or (pdx < 0 and dirs & SWIPE_LEFT):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
min(pdxa / _SWIPE_DISTANCE, 1),
)
elif pdxa < pdya and self.directions & SWIPE_VERTICAL:
# Vertical direction
if (pdy > 0 and self.directions & SWIPE_DOWN) or (
pdy < 0 and self.directions & SWIPE_UP
):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1,
)
)
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
# vertical direction
if (pdy > 0 and dirs & SWIPE_DOWN) or (pdy < 0 and dirs & SWIPE_UP):
ui.display.backlight(
ui.lerpi(
self.light_origin,
self.light_target,
min(pdya / _SWIPE_DISTANCE, 1),
)
)
elif event == io.TOUCH_START and contains(self.area, pos):
self.start_pos = pos
self.light_origin = ui.BACKLIGHT_NORMAL
def on_touch_end(self, x, y):
if self.start_x is None:
return # not started in our area
dirs = self.directions
pdx = x - self.start_x
pdy = y - self.start_y
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
# horizontal direction
ratio = min(pdxa / _SWIPE_DISTANCE, 1)
if ratio * 100 >= _SWIPE_TRESHOLD:
if pdx > 0 and dirs & SWIPE_RIGHT:
self.on_swipe(SWIPE_RIGHT)
return
elif pdx < 0 and dirs & SWIPE_LEFT:
self.on_swipe(SWIPE_LEFT)
return
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
# vertical direction
ratio = min(pdya / _SWIPE_DISTANCE, 1)
if ratio * 100 >= _SWIPE_TRESHOLD:
if pdy > 0 and dirs & SWIPE_DOWN:
self.on_swipe(SWIPE_DOWN)
return
elif pdy < 0 and dirs & SWIPE_UP:
self.on_swipe(SWIPE_UP)
return
# no swipe detected, reset the state
ui.display.backlight(self.light_origin)
self.start_x = None
self.start_y = None
def on_swipe(self, swipe):
raise ui.Result(swipe)
elif event == io.TOUCH_END and self.start_pos is not None:
pdx = pos[0] - self.start_pos[0]
pdy = pos[1] - self.start_pos[1]
pdxa = abs(pdx)
pdya = abs(pdy)
if pdxa > pdya and self.directions & SWIPE_HORIZONTAL:
# Horizontal direction
ratio = pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1
if ratio * 100 >= self.treshold:
if pdx > 0 and self.directions & SWIPE_RIGHT:
return SWIPE_RIGHT
elif pdx < 0 and self.directions & SWIPE_LEFT:
return SWIPE_LEFT
elif pdxa < pdya and self.directions & SWIPE_VERTICAL:
# Vertical direction
ratio = pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1
if ratio * 100 >= self.treshold:
if pdy > 0 and self.directions & SWIPE_DOWN:
return SWIPE_DOWN
elif pdy < 0 and self.directions & SWIPE_UP:
return SWIPE_UP
# No swipe, reset the state
self.start_pos = None
ui.display.backlight(self.light_origin)
def __iter__(self):
try:
touch = loop.wait(io.TOUCH)
while True:
event, x, y = yield touch
self.dispatch(event, x, y)
except ui.Result as result:
return result.value

@ -33,13 +33,13 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
has_next_word = word_index < len(words) - 1
if isinstance(word, int):
if word in [BR, BR_HALF]:
if word is BR or word is BR_HALF:
# line break or half-line break
if offset_y >= OFFSET_Y_MAX:
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
return
offset_x = TEXT_MARGIN_LEFT
offset_y += TEXT_LINE_HEIGHT if word == BR else TEXT_LINE_HEIGHT_HALF
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
elif word in FONTS:
# change of font style
font = word
@ -108,7 +108,7 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
offset_x += SPACE
class Text(ui.Widget):
class Text(ui.Control):
def __init__(
self,
header_text: str,
@ -123,6 +123,7 @@ class Text(ui.Widget):
self.max_lines = max_lines
self.new_lines = new_lines
self.content = []
self.repaint = True
def normal(self, *content):
self.content.append(ui.NORMAL)
@ -146,8 +147,8 @@ class Text(ui.Widget):
def br_half(self):
self.content.append(BR_HALF)
def render(self):
if self.tainted:
def on_render(self):
if self.repaint:
ui.header(
self.header_text,
self.header_icon,
@ -156,4 +157,4 @@ class Text(ui.Widget):
self.icon_color,
)
render_text(self.content, self.new_lines, self.max_lines)
self.tainted = False
self.repaint = False

@ -1,56 +1,28 @@
from micropython import const
from trezor import ui
from trezor.ui.button import Button
from trezor import loop, ui
from trezor.ui import Widget
from trezor.ui.button import BTN_CLICKED, Button
if __debug__:
from apps.debug import input_signal
_W12 = const(12)
_W18 = const(18)
_W24 = const(24)
class WordSelector(Widget):
class WordSelector(ui.Layout):
def __init__(self, content):
self.content = content
self.w12 = Button(
ui.grid(6, n_y=4, n_x=3, cells_y=2), str(_W12), style=ui.BTN_KEY
)
self.w18 = Button(
ui.grid(7, n_y=4, n_x=3, cells_y=2), str(_W18), style=ui.BTN_KEY
)
self.w24 = Button(
ui.grid(8, n_y=4, n_x=3, cells_y=2), str(_W24), style=ui.BTN_KEY
)
def taint(self):
super().taint()
self.w12.taint()
self.w18.taint()
self.w24.taint()
def render(self):
self.w12.render()
self.w18.render()
self.w24.render()
def touch(self, event, pos):
if self.w12.touch(event, pos) == BTN_CLICKED:
return _W12
if self.w18.touch(event, pos) == BTN_CLICKED:
return _W18
if self.w24.touch(event, pos) == BTN_CLICKED:
return _W24
async def __iter__(self):
if __debug__:
result = await loop.spawn(super().__iter__(), self.content, input_signal)
if isinstance(result, str):
return int(result)
else:
return result
else:
return await loop.spawn(super().__iter__(), self.content)
self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12")
self.w12.on_click = self.on_w12
self.w18 = Button(ui.grid(7, n_y=4, n_x=3, cells_y=2), "18")
self.w18.on_click = self.on_w18
self.w24 = Button(ui.grid(8, n_y=4, n_x=3, cells_y=2), "24")
self.w24.on_click = self.on_w24
def dispatch(self, event, x, y):
self.content.dispatch(event, x, y)
self.w12.dispatch(event, x, y)
self.w18.dispatch(event, x, y)
self.w24.dispatch(event, x, y)
def on_w12(self):
raise ui.Result(12)
def on_w18(self):
raise ui.Result(18)
def on_w24(self):
raise ui.Result(24)

@ -18,10 +18,12 @@ if __debug__:
import uos
TEST = int(uos.getenv("TREZOR_TEST") or "0")
DISABLE_FADE = int(uos.getenv("TREZOR_DISABLE_FADE") or "0")
SAVE_SCREEN = int(uos.getenv("TREZOR_SAVE_SCREEN") or "0")
LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0")
else:
TEST = 0
DISABLE_FADE = 0
SAVE_SCREEN = 0
LOG_MEMORY = 0

@ -2,12 +2,10 @@ from trezor import loop
workflows = []
layouts = []
layout_signal = loop.signal()
default = None
default_layout = None
# HACK: workaround way to stop the WebAuthn layout from the outside
webauthn_stop_signal = loop.signal()
def onstart(w):
workflows.append(w)
@ -54,7 +52,6 @@ def restartdefault():
def onlayoutstart(l):
closedefault()
layouts.append(l)
webauthn_stop_signal.send(None)
def onlayoutclose(l):

Loading…
Cancel
Save