1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-06-19 14:38:47 +00:00

core: revamp the UI subsystem

This commit is contained in:
Jan Pochyla 2019-05-13 15:06:34 +02:00
parent 2f4c123466
commit 36534325f0
53 changed files with 1715 additions and 1933 deletions

View File

@ -1,13 +1,12 @@
from micropython import const from micropython import const
from trezor import ui from trezor import ui
from trezor.messages import ButtonRequestType, MessageType from trezor.ui.scroll import Paginated
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.text import Text from trezor.ui.text import Text
from trezor.utils import chunks, format_amount from trezor.utils import chunks, format_amount
from apps.common.confirm import confirm, hold_to_confirm
def format_coin_amount(amount): def format_coin_amount(amount):
return "%s %s" % (format_amount(amount, 6), "ADA") return "%s %s" % (format_amount(amount, 6), "ADA")
@ -16,107 +15,34 @@ def format_coin_amount(amount):
async def confirm_sending(ctx, amount, to): async def confirm_sending(ctx, amount, to):
to_lines = list(chunks(to, 17)) 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.normal("Confirm sending:")
t1.bold(format_coin_amount(amount)) t1.bold(format_coin_amount(amount))
t1.normal("to:") t1.normal("to:")
t1.bold(to_lines[0]) t1.bold(to_lines[0])
pages = [t1]
LINES_PER_PAGE = 4 PER_PAGE = const(4)
pages = [t1]
if len(to_lines) > 1: 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: 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: for line in page:
t.bold(line) t.bold(line)
pages.append(t) pages.append(t)
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck) return await confirm(ctx, Paginated(pages))
paginator = paginate(create_renderer(ConfirmDialog), len(pages), const(0), pages)
return await ctx.wait(paginator) == CONFIRMED
async def confirm_transaction(ctx, amount, fee, network_name): 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.normal("Total amount:")
t1.bold(format_coin_amount(amount)) t1.bold(format_coin_amount(amount))
t1.normal("including fee:") t1.normal("including fee:")
t1.bold(format_coin_amount(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.normal("Network:")
t2.bold(network_name) t2.bold(network_name)
pages = [t1, t2] return await hold_to_confirm(ctx, Paginated([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()

View File

@ -1,29 +1,59 @@
from trezor import ui, wire from trezor import wire
from trezor.messages import ButtonRequestType, MessageType from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest 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(
async def confirm(ctx, content, code=None, *args, **kwargs): ctx,
if code is None: content,
code = ButtonRequestType.Other 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) 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(
async def hold_to_confirm(ctx, content, code=None, *args, **kwargs): ctx,
if code is None: content,
code = ButtonRequestType.Other 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) 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): async def require_confirm(*args, **kwargs):

View File

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

View File

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

View File

@ -15,14 +15,12 @@ async def validate_path(ctx, validate_func, keychain, path, curve, **kwargs):
async def show_path_warning(ctx, path: list): 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.normal("Path")
text.mono(*break_address_n_to_lines(path)) text.mono(*break_address_n_to_lines(path))
text.normal("is unknown.") text.normal("is unknown.")
text.normal("Are you sure?") text.normal("Are you sure?")
return await require_confirm( return await require_confirm(ctx, text, ButtonRequestType.UnknownDerivationPath)
ctx, text, code=ButtonRequestType.UnknownDerivationPath
)
def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool: 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: def is_hardened(i: int) -> bool:
if i & HARDENED: if i & HARDENED:
return True return True
return False else:
return False
def break_address_n_to_lines(address_n: list) -> list: def break_address_n_to_lines(address_n: list) -> list:

View File

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

View File

@ -1,6 +1,5 @@
from trezor import loop, res, ui from trezor import loop
from trezor.ui.confirm import CONFIRMED, ConfirmDialog from trezor.ui.pin import CANCELLED, PinDialog
from trezor.ui.pin import PinMatrix
if __debug__: if __debug__:
from apps.debug import input_signal from apps.debug import input_signal
@ -10,71 +9,25 @@ class PinCancelled(Exception):
pass pass
@ui.layout
async def request_pin( 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: ) -> str:
def onchange(): if attempts_remaining is None:
c = dialog.cancel subprompt = None
if matrix.pin: elif attempts_remaining == 1:
back = res.load(ui.ICON_BACK) subprompt = "This is your last attempt"
if c.content is not back: else:
c.normal_style = ui.BTN_CLEAR["normal"] subprompt = "%s attempts remaining" % attempts_remaining
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 dialog = PinDialog(prompt, subprompt, allow_cancel)
if matrix.pin:
if not c.is_enabled():
c.enable()
c.taint()
else:
if c.is_enabled():
c.disable()
c.taint()
c.render()
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()
while True: while True:
if __debug__: if __debug__:
result = await loop.spawn(dialog, input_signal) result = await loop.spawn(dialog, input_signal)
if isinstance(result, str):
return result
else: else:
result = await dialog result = await dialog
if result == CONFIRMED: if result is CANCELLED:
if not matrix.pin:
continue
return matrix.pin
elif matrix.pin: # reset
matrix.change("")
continue
else: # cancel
raise PinCancelled() raise PinCancelled()
return result

View File

@ -1,4 +1,4 @@
from trezor import ui, wire from trezor import wire
from trezor.crypto import bip32 from trezor.crypto import bip32
from apps.common import HARDENED, cache, mnemonic, storage 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") raise wire.ProcessError("Device is not initialized")
seed = cache.get_seed() seed = cache.get_seed()
if seed is None: 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) keychain = Keychain(seed, namespaces)
return keychain 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( def derive_node_without_passphrase(
path: list, curve_name: str = "secp256k1" path: list, curve_name: str = "secp256k1"
) -> bip32.HDNode: ) -> bip32.HDNode:
@ -102,3 +88,7 @@ def derive_node_without_passphrase(
def remove_ed25519_prefix(pubkey: bytes) -> bytes: def remove_ed25519_prefix(pubkey: bytes) -> bytes:
# 0x01 prefix is not part of the actual public key, hence removed # 0x01 prefix is not part of the actual public key, hence removed
return pubkey[1:] return pubkey[1:]
def _path_hardened(path: list) -> bool:
return all(i & HARDENED for i in path)

View File

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

View File

@ -6,14 +6,14 @@ from apps.common.confirm import require_confirm
async def require_get_public_key(ctx, public_key): 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) 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): 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("You are about")
text.normal("to sign {}".format(num_actions)) text.normal("to sign {}".format(num_actions))
text.normal("action(s).") text.normal("action(s).")
return await require_confirm(ctx, text, code=ButtonRequestType.SignTx) return await require_confirm(ctx, text, ButtonRequestType.SignTx)

View File

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

View File

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

View File

@ -10,7 +10,7 @@ from apps.common.layout import show_pubkey, split_address
async def require_confirm_tx(ctx, to, value): 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.bold(format_coin_amount(value))
text.normal("to") text.normal("to")
text.mono(*split_address(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): 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("Do you really want to")
text.normal("register a delegate?") text.normal("register a delegate?")
text.bold(*chunks(delegate_name, 20)) 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): 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)) text.normal(*get_vote_tx_text(votes))
return await require_confirm(ctx, text, ButtonRequestType.SignTx) 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): 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("Keys group length: %s" % len(multisignature.keys_group))
text.normal("Life time: %s" % multisignature.life_time) text.normal("Life time: %s" % multisignature.life_time)
text.normal("Min: %s" % multisignature.min) 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): 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.bold(format_coin_amount(value))
text.normal("fee:") text.normal("fee:")
text.bold(format_coin_amount(fee)) text.bold(format_coin_amount(fee))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
from trezor import ui from trezor import ui
from trezor.messages import ButtonRequestType from trezor.messages import ButtonRequestType
from trezor.messages.Success import Success 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 trezor.ui.text import Text
from apps.common import storage from apps.common import storage
@ -8,17 +10,15 @@ from apps.common.confirm import require_hold_to_confirm
async def wipe_device(ctx, msg): async def wipe_device(ctx, msg):
text = Text("Wipe device", ui.ICON_WIPE, ui.RED)
text = Text("Wipe device", ui.ICON_WIPE, icon_color=ui.RED)
text.normal("Do you really want to", "wipe the device?", "") text.normal("Do you really want to", "wipe the device?", "")
text.bold("All data will be lost.") text.bold("All data will be lost.")
await require_hold_to_confirm( await require_hold_to_confirm(
ctx, ctx,
text, text,
code=ButtonRequestType.WipeDevice, ButtonRequestType.WipeDevice,
button_style=ui.BTN_CANCEL, confirm_style=ButtonCancel,
loader_style=ui.LDR_DANGER, loader_style=LoaderDanger,
) )
storage.wipe() storage.wipe()

View File

@ -1,7 +1,43 @@
from trezor import res, ui, utils from trezor import loop, ui, utils
from trezor.messages import ButtonRequestType from trezor.messages import ButtonRequestType, MessageType
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.ui.text import Text 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): def paginate_lines(lines, lines_per_page=5):
@ -28,94 +64,9 @@ def paginate_lines(lines, lines_per_page=5):
return pages 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): def format_amount(value):
return "%s XMR" % utils.format_amount(value, 12) return "%s XMR" % utils.format_amount(value, 12)
def split_address(address): def split_address(address):
return chunks(address, 16) return utils.chunks(address, 16)

View File

@ -2,6 +2,7 @@ from ubinascii import hexlify
from trezor import ui, wire from trezor import ui, wire
from trezor.messages import ButtonRequestType from trezor.messages import ButtonRequestType
from trezor.ui.popup import Popup
from trezor.ui.text import Text from trezor.ui.text import Text
from trezor.utils import chunks from trezor.utils import chunks
@ -12,25 +13,25 @@ DUMMY_PAYMENT_ID = b"\x00" * 8
async def require_confirm_watchkey(ctx): 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?") content.normal("Do you really want to", "export watch-only", "credentials?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx) return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_keyimage_sync(ctx): 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?") content.normal("Do you really want to", "sync key images?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx) return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_live_refresh(ctx): 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?") content.normal("Do you really want to", "start refresh?")
return await require_confirm(ctx, content, ButtonRequestType.SignTx) return await require_confirm(ctx, content, ButtonRequestType.SignTx)
async def require_confirm_tx_key(ctx, export_key=False): 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"] txt = ["Do you really want to"]
if export_key: if export_key:
txt.append("export tx_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): 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)) content.bold(common.format_amount(fee))
await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput) 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): async def transaction_step(state, step, sub_step=None):
info = []
if step == 0: if step == 0:
info = ["Signing..."] info = ["Signing..."]
elif step == 100: elif step == 100:
@ -139,43 +181,16 @@ async def transaction_step(state, step, sub_step=None):
info = ["Processing..."] info = ["Processing..."]
state.progress_cur += 1 state.progress_cur += 1
await Popup(TransactionStep(state, info))
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()
@ui.layout_no_slide
async def keyimage_sync_step(ctx, current, total_num): async def keyimage_sync_step(ctx, current, total_num):
if current is None: if current is None:
return return
ui.display.clear() await Popup(KeyImageSyncStep(current, total_num))
text = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE)
text.render()
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): async def live_refresh_step(ctx, current):
if current is None: if current is None:
return return
ui.display.clear() await Popup(LiveRefreshStep(current))
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()

View File

@ -10,7 +10,7 @@ from apps.common.confirm import require_confirm, require_hold_to_confirm
async def require_confirm_text(ctx, action: str): async def require_confirm_text(ctx, action: str):
content = action.split(" ") 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) text.normal(*content)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) 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): 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) text.normal(*content)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_final(ctx, fee: int): 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.normal("Sign this transaction")
text.bold("and pay %s XEM" % format_amount(fee, NEM_MAX_DIVISIBILITY)) text.bold("and pay %s XEM" % format_amount(fee, NEM_MAX_DIVISIBILITY))
text.normal("for network fee?") text.normal("for network fee?")

View File

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

View File

@ -53,7 +53,7 @@ async def ask_aggregate_modification(
async def _require_confirm_address(ctx, action: str, address: str): 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.normal(action)
text.mono(*split_address(address)) text.mono(*split_address(address))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)

View File

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

View File

@ -10,14 +10,14 @@ from apps.common.layout import split_address
async def require_confirm_fee(ctx, fee): 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.normal("Transaction fee:")
text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP") text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP")
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_destination_tag(ctx, tag): 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.normal("Destination tag:")
text.bold(str(tag)) text.bold(str(tag))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) 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): 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.bold(format_amount(value, helpers.DIVISIBILITY) + " XRP")
text.normal("to") text.normal("to")
text.mono(*split_address(to)) text.mono(*split_address(to))

View File

@ -9,7 +9,7 @@ from apps.stellar import consts
async def require_confirm_init( async def require_confirm_init(
ctx, address: str, network_passphrase: str, accounts_match: bool 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") text.normal("Initialize signing with")
if accounts_match: if accounts_match:
text.normal("your account") text.normal("your account")
@ -19,14 +19,14 @@ async def require_confirm_init(
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
network = get_network_warning(network_passphrase) network = get_network_warning(network_passphrase)
if network: 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.normal("Transaction is on")
text.bold(network) text.bold(network)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_memo(ctx, memo_type: int, memo_text: str): 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: if memo_type == consts.MEMO_TYPE_TEXT:
text.bold("Memo (TEXT)") text.bold("Memo (TEXT)")
elif memo_type == consts.MEMO_TYPE_ID: 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" op_str = str(num_operations) + " operation"
if num_operations > 1: if num_operations > 1:
op_str += "s" 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("Sign this transaction")
text.normal("made up of " + op_str) text.normal("made up of " + op_str)
text.bold("and pay " + format_amount(fee)) text.bold("and pay " + format_amount(fee))

View File

@ -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): 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.bold("Source account:")
text.mono(*split(source_account)) text.mono(*split(source_account))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -32,7 +32,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
t = "Allow Trust" t = "Allow Trust"
else: else:
t = "Revoke Trust" 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.bold(t)
text.normal("of %s by:" % op.asset_code) text.normal("of %s by:" % op.asset_code)
text.mono(*split(trim_to_rows(op.trusted_account, 3))) 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): 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.bold("Account Merge")
text.normal("All XLM will be sent to:") text.normal("All XLM will be sent to:")
text.mono(*split(trim_to_rows(op.destination_account, 3))) 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): 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.bold("Bump Sequence")
text.normal("Set sequence to") text.normal("Set sequence to")
text.mono(str(op.bump_to)) text.mono(str(op.bump_to))
@ -61,7 +61,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
t = "Delete Trust" t = "Delete Trust"
else: else:
t = "Add Trust" 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.bold(t)
text.normal("Asset: %s" % op.asset.code) text.normal("Asset: %s" % op.asset.code)
text.normal("Amount: %s" % format_amount(op.limit, ticker=False)) 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): 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.bold("Create Account")
text.normal("with %s" % format_amount(op.starting_balance)) text.normal("with %s" % format_amount(op.starting_balance))
text.mono(*split(trim_to_rows(op.new_account, 3))) 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): 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.bold(title)
text.normal( text.normal(
"Sell %s %s" % (format_amount(op.amount, ticker=False), op.selling_asset.code) "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" title = "Set"
else: else:
title = "Clear" 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.bold("%s data value key" % title)
text.mono(*split(op.key)) text.mono(*split(op.key))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.value: if op.value:
digest = sha256(op.value).digest() digest = sha256(op.value).digest()
digest_str = hexlify(digest).decode() 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.bold("Value (SHA-256):")
text.mono(*split(digest_str)) text.mono(*split(digest_str))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp): 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("Path Pay %s" % format_amount(op.destination_amount, ticker=False))
text.bold("%s to:" % format_asset_code(op.destination_asset)) text.bold("%s to:" % format_asset_code(op.destination_asset))
text.mono(*split(trim_to_rows(op.destination_account, 3))) text.mono(*split(trim_to_rows(op.destination_account, 3)))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
await confirm_asset_issuer(ctx, op.destination_asset) await confirm_asset_issuer(ctx, op.destination_asset)
# confirm what the sender is using to pay # 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.normal("Pay using")
text.bold(format_amount(op.send_max, ticker=False)) text.bold(format_amount(op.send_max, ticker=False))
text.bold(format_asset_code(op.send_asset)) 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): 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("Pay %s" % format_amount(op.amount, ticker=False))
text.bold("%s to:" % format_asset_code(op.asset)) text.bold("%s to:" % format_asset_code(op.asset))
text.mono(*split(trim_to_rows(op.destination_account, 3))) 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): async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
if op.inflation_destination_account: 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.bold("Set Inflation Destination")
text.mono(*split(op.inflation_destination_account)) text.mono(*split(op.inflation_destination_account))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.clear_flags: if op.clear_flags:
t = _format_flags(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.bold("Clear Flags")
text.mono(*t) text.mono(*t)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.set_flags: if op.set_flags:
t = _format_flags(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.bold("Set Flags")
text.mono(*t) text.mono(*t)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
thresholds = _format_thresholds(op) thresholds = _format_thresholds(op)
if thresholds: 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.bold("Account Thresholds")
text.mono(*thresholds) text.mono(*thresholds)
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
if op.home_domain: 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.bold("Home Domain")
text.mono(*split(op.home_domain)) text.mono(*split(op.home_domain))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -192,7 +192,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
else: else:
t = "Remove Signer (%s)" t = "Remove Signer (%s)"
if op.signer_type == consts.SIGN_TYPE_ACCOUNT: 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.bold(t % "acc")
text.mono(*split(helpers.address_from_public_key(op.signer_key))) text.mono(*split(helpers.address_from_public_key(op.signer_key)))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
@ -201,7 +201,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
signer_type = "auth" signer_type = "auth"
else: else:
signer_type = "hash" 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.bold(t % signer_type)
text.mono(*split(hexlify(op.signer_key).decode())) text.mono(*split(hexlify(op.signer_key).decode()))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) 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): async def confirm_asset_issuer(ctx, asset: StellarAssetType):
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE: if asset is None or asset.type == consts.ASSET_TYPE_NATIVE:
return 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.bold("%s issuer:" % asset.code)
text.mono(*split(asset.issuer)) text.mono(*split(asset.issuer))
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)

View File

@ -26,6 +26,6 @@ async def get_public_key(ctx, msg, keychain):
async def _show_tezos_pubkey(ctx, pubkey): async def _show_tezos_pubkey(ctx, pubkey):
lines = chunks(pubkey, 18) 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) text.mono(*lines)
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey) return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)

View File

@ -1,10 +1,6 @@
from micropython import const from trezor import ui
from trezor.messages import ButtonRequestType
from trezor import ui, wire from trezor.ui.scroll import Paginated
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.ui.text import Text from trezor.ui.text import Text
from trezor.utils import chunks, format_amount 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): 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.bold(format_tezos_amount(value))
text.normal("to") text.normal("to")
text.mono(*split_address(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): 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.normal("Amount:")
text.bold(format_tezos_amount(value)) text.bold(format_tezos_amount(value))
text.normal("Fee:") text.normal("Fee:")
@ -30,14 +26,14 @@ async def require_confirm_fee(ctx, value, fee):
async def require_confirm_origination(ctx, address): 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.normal("Address:")
text.mono(*split_address(address)) text.mono(*split_address(address))
return await require_confirm(ctx, text, ButtonRequestType.SignTx) return await require_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_origination_fee(ctx, balance, fee): 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.normal("Balance:")
text.bold(format_tezos_amount(balance)) text.bold(format_tezos_amount(balance))
text.normal("Fee:") text.normal("Fee:")
@ -46,21 +42,21 @@ async def require_confirm_origination_fee(ctx, balance, fee):
async def require_confirm_delegation_baker(ctx, baker): 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.normal("Baker address:")
text.mono(*split_address(baker)) text.mono(*split_address(baker))
return await require_confirm(ctx, text, ButtonRequestType.SignTx) return await require_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_set_delegate(ctx, fee): 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.normal("Fee:")
text.bold(format_tezos_amount(fee)) text.bold(format_tezos_amount(fee))
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx) await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
async def require_confirm_register_delegate(ctx, address, fee): 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.bold("Fee: " + format_tezos_amount(fee))
text.normal("Address:") text.normal("Address:")
text.mono(*split_address(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) 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): async def require_confirm_proposals(ctx, proposals):
await ctx.call(ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck) if len(proposals) > 1:
first_page = const(0) title = "Submit proposals"
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")
else: else:
content.render() title = "Submit proposal"
await animate_swipe()
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -31,6 +31,10 @@ SIZE = Display.FONT_SIZE
WIDTH = Display.WIDTH WIDTH = Display.WIDTH
HEIGHT = Display.HEIGHT HEIGHT = Display.HEIGHT
# viewport margins
VIEWX = const(6)
VIEWY = const(9)
def lerpi(a: int, b: int, t: float) -> int: def lerpi(a: int, b: int, t: float) -> int:
return int(a + t * (b - a)) 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 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): def pulse(delay: int):
while True: # normalize sin from interval -1:1 to 0:1
# normalize sin from interval -1:1 to 0:1 return 0.5 + 0.5 * math.sin(utime.ticks_us() / delay)
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)
async def click() -> tuple: async def click() -> tuple:
@ -107,53 +77,22 @@ async def click() -> tuple:
return pos return pos
async def backlight_slide(val: int, delay: int = 35000, step: int = 20): def backlight_fade(val: int, delay: int = 14000, step: int = 15):
sleep = loop.sleep(delay) if __debug__:
if utils.DISABLE_FADE:
display.backlight(val)
return
current = display.backlight() current = display.backlight()
for i in range(current, val, -step if current > val else step): if current > val:
display.backlight(i) step = -step
yield sleep for i in range(current, val, step):
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):
display.backlight(i) display.backlight(i)
utime.sleep_us(delay) 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( def header(
title: str, title: str,
icon: bytes = style.ICON_DEFAULT, icon: str = style.ICON_DEFAULT,
fg: int = style.FG, fg: int = style.FG,
bg: int = style.BG, bg: int = style.BG,
ifg: int = style.GREEN, ifg: int = style.GREEN,
@ -163,10 +102,6 @@ def header(
display.text(44, 35, title, BOLD, fg, bg) display.text(44, 35, title, BOLD, fg, bg)
VIEWX = const(6)
VIEWY = const(9)
def grid( def grid(
i: int, i: int,
n_x: int = 3, 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) return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y)
class Widget: def in_area(area: tuple, x: int, y: int) -> bool:
tainted = True ax, ay, aw, ah = area
return ax <= x <= ax + aw and ay <= y <= ay + ah
def taint(self):
self.tainted = True
def render(self): # 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 pass
def touch(self, event, pos): def on_touch_start(self, x, y):
pass pass
def __iter__(self): def on_touch_move(self, x, y):
pass
def on_touch_end(self, x, y):
pass
_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) touch = loop.wait(io.TOUCH)
result = None while True:
while result is None: event, x, y = yield touch
self.render() self.dispatch(event, x, y)
event, *pos = yield touch self.dispatch(RENDER, 0, 0)
result = self.touch(event, pos)
return result 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

View File

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

View File

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

View File

@ -1,21 +1,10 @@
from trezor.ui import Widget from trezor import ui
class Container(Widget): class Container(ui.Control):
def __init__(self, *children): def __init__(self, *children):
self.children = children self.children = children
def taint(self): def dispatch(self, event, x, y):
super().taint()
for child in self.children: for child in self.children:
child.taint() child.dispatch(event, x, y)
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

View File

@ -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)

View File

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

View File

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

View File

@ -1,17 +1,14 @@
from micropython import const
from trezor import res, ui from trezor import res, ui
from trezor.ui import display from trezor.ui.button import Button, ButtonMono, ButtonMonoDark
from trezor.ui.button import BTN_CLICKED, Button
ITEMS_PER_PAGE = 10 _ITEMS_PER_PAGE = const(10)
PLUS_BUTTON_POSITION = 11 _BACK_BUTTON_POSITION = const(9)
BACK_BUTTON_POSITION = 9 _PLUS_BUTTON_POSITION = const(11)
def digit_area(i): class NumPad(ui.Layout):
return ui.grid(i + 3) # skip the first line
class NumPad(ui.Widget):
def __init__(self, label: str, start: int, end: int): def __init__(self, label: str, start: int, end: int):
""" """
Generates a numpad with numbers from `start` to `end` excluding. Generates a numpad with numbers from `start` to `end` excluding.
@ -20,50 +17,63 @@ class NumPad(ui.Widget):
self.start = start self.start = start
self.end = end self.end = end
self.page = 0 self.page = 0
self.buttons = generate_buttons(self.start, self.end, self.page, self)
self._generate_buttons() def dispatch(self, event, x, y):
for button in self.buttons:
def render(self): button.dispatch(event, x, y)
for btn in self.buttons: if event is ui.RENDER:
btn.render() # render header label
ui.display.text_center(
# header label ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG
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
) )
back = Button( def on_back(self):
digit_area(BACK_BUTTON_POSITION), self.page -= 1
res.load(ui.ICON_BACK), self.buttons = generate_buttons(self.start, self.end, self.page, self)
style=ui.BTN_KEY_DARK, ui.display.clear() # we need to clear old buttons
)
if self.page == 0: def on_plus(self):
back.disable() self.page += 1
self.buttons.append(back) 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

View File

@ -1,8 +1,9 @@
from micropython import const from micropython import const
from trezor import io, loop, res, ui from trezor import io, loop, res, ui
from trezor.messages import PassphraseSourceType
from trezor.ui import display 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 from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
SPACE = res.load(ui.ICON_SPACE) SPACE = res.load(ui.ICON_SPACE)
@ -21,45 +22,60 @@ def digit_area(i):
return ui.grid(i + 3) # skip the first line 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): def render_scrollbar(page):
bbox = const(240) BBOX = const(240)
size = const(8) SIZE = const(8)
pages = len(KEYBOARD_KEYS)
padding = 12 padding = 12
page_count = len(KEYBOARD_KEYS) if pages * padding > BBOX:
padding = BBOX // pages
if page_count * padding > bbox: x = (BBOX // 2) - (pages // 2) * padding
padding = bbox // page_count Y = const(44)
x = (bbox // 2) - (page_count // 2) * padding for i in range(0, pages):
y = 44 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)
for i in range(0, page_count):
if i != page: class KeyButton(Button):
ui.display.bar_radius( def __init__(self, area, content, keyboard):
x + i * padding, y, size, size, ui.DARK_GREY, ui.BG, size // 2 self.keyboard = keyboard
) super().__init__(area, content)
ui.display.bar_radius(x + page * padding, y, size, size, ui.FG, ui.BG, size // 2)
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
def key_buttons(keys, keyboard):
return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)]
class Input(Button): class Input(Button):
def __init__(self, area: tuple, content: str = ""): def __init__(self, area, content):
super().__init__(area, content) super().__init__(area, content)
self.pending = False self.pending = False
self.disable() self.disable()
def edit(self, content: str, pending: bool): def edit(self, content, pending):
self.content = content self.content = content
self.pending = pending self.pending = pending
self.taint() self.repaint = True
def render_content(self, s, ax, ay, aw, ah): def render_content(self, s, ax, ay, aw, ah):
text_style = s["text-style"] text_style = s.text_style
fg_color = s["fg-color"] fg_color = s.fg_color
bg_color = s["bg-color"] bg_color = s.bg_color
p = self.pending # should we draw the pending marker? p = self.pending # should we draw the pending marker?
t = self.content # input content t = self.content # input content
@ -76,142 +92,167 @@ class Input(Button):
if p: # pending marker if p: # pending marker
pw = display.text_width(t[-1:], text_style) 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 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): def __init__(self, text):
self.text = text self.text = text
self.repaint = True
def render(self): def on_render(self):
if self.tainted: if self.repaint:
display.bar(0, 0, ui.WIDTH, 48, ui.BG) display.bar(0, 0, ui.WIDTH, 48, ui.BG)
display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, 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): class PassphraseKeyboard(ui.Layout):
def __init__(self, prompt, page=1): def __init__(self, prompt, max_length, page=1):
self.prompt = Prompt(prompt) self.prompt = Prompt(prompt)
self.max_length = max_length
self.page = page self.page = page
self.input = Input(ui.grid(0, n_x=1, n_y=6), "") 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): self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear)
super().taint() self.back.on_click = self.on_back_click
self.prompt.taint() self.back.disable()
self.input.taint()
self.back.taint()
self.done.taint()
for btn in self.keys:
btn.taint()
def render(self): self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm)
# passphrase or prompt 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: if self.input.content:
self.input.render() self.input.dispatch(event, x, y)
else: else:
self.prompt.render() self.prompt.dispatch(event, x, y)
render_scrollbar(self.page) self.back.dispatch(event, x, y)
# buttons self.done.dispatch(event, x, y)
self.back.render()
self.done.render()
for btn in self.keys: for btn in self.keys:
btn.render() btn.dispatch(event, x, y)
def touch(self, event, pos): if event == ui.RENDER:
render_scrollbar(self.page)
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 content = self.input.content
if self.back.touch(event, pos) == BTN_CLICKED: if content:
if content: self.edit(content[:-1])
# backspace, delete the last character of input else:
self.edit(content[:-1]) self.on_cancel()
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) def on_key_click(self, button: KeyButton):
return # 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
def edit(self, content, button=None, index=0): # character.
if button and len(button.content) == 1: button_text = button.get_text_content()
# one-letter buttons are never pending if self.pending_button is button:
button = None index = (self.pending_index + 1) % len(button_text)
prefix = self.input.content[:-1]
else:
index = 0 index = 0
self.pbutton = button prefix = self.input.content
self.pindex = index if len(button_text) > 1:
self.input.edit(content, button is not None) 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: if content:
self.back.enable() self.back.enable()
else: else:
self.back.disable() self.back.disable()
self.prompt.taint() self.prompt.repaint = True
async def __iter__(self): async def handle_input(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)
touch = loop.wait(io.TOUCH) touch = loop.wait(io.TOUCH)
wait_timeout = loop.spawn(touch, timeout) timeout = loop.sleep(1000 * 1000 * 1)
wait_touch = loop.spawn(touch) spawn_touch = loop.spawn(touch)
content = None spawn_timeout = loop.spawn(touch, timeout)
while content is None:
self.render()
if self.pbutton is not None:
wait = wait_timeout
else:
wait = wait_touch
result = await wait
if touch in wait.finished:
event, *pos = result
content = self.touch(event, pos)
else:
# disable the pending buttons
self.edit(self.input.content)
return content
async def change_page(self): while True:
swipe = await Swipe(directions=SWIPE_HORIZONTAL) if self.pending_button is not None:
spawn = spawn_timeout
else:
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
self.on_timeout()
async def handle_paging(self):
swipe = await Swipe(SWIPE_HORIZONTAL)
if swipe == SWIPE_LEFT: if swipe == SWIPE_LEFT:
self.page = (self.page + 1) % len(KEYBOARD_KEYS) self.page = (self.page + 1) % len(KEYBOARD_KEYS)
else: else:
self.page = (self.page - 1) % len(KEYBOARD_KEYS) self.page = (self.page - 1) % len(KEYBOARD_KEYS)
self.keys = key_buttons(KEYBOARD_KEYS[self.page]) self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
self.back.taint() self.back.repaint = True
self.done.taint() self.done.repaint = True
self.input.taint() self.input.repaint = True
self.prompt.taint() 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)

View File

@ -1,9 +1,15 @@
from micropython import const from micropython import const
from trezor import ui from trezor import res, ui
from trezor.crypto import random from trezor.crypto import random
from trezor.ui import display 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): def digit_area(i):
@ -15,78 +21,119 @@ def digit_area(i):
def generate_digits(): def generate_digits():
digits = list(range(0, 10)) # 0-9 digits = list(range(0, 10)) # 0-9
random.shuffle(digits) 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): class PinInput(ui.Control):
def __init__(self, label, sublabel, pin="", maxlength=9): def __init__(self, prompt, subprompt, pin):
self.label = label self.prompt = prompt
self.sublabel = sublabel self.subprompt = subprompt
self.pin = pin self.pin = pin
self.maxlength = maxlength self.repaint = True
self.digits = generate_digits()
# we lay out the buttons top-left to bottom-right, but the order of the def on_render(self):
# digits is defined as bottom-left to top-right (on numpad) if self.repaint:
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3] if self.pin:
self.render_pin()
else:
self.render_prompt()
self.repaint = False
self.pin_buttons = [ def render_pin(self):
Button(digit_area(i), str(d)) for i, d in enumerate(reordered_digits) display.bar(0, 0, ui.WIDTH, 50, ui.BG)
] count = len(self.pin)
self.onchange = None 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
)
def taint(self): def render_prompt(self):
super().taint() display.bar(0, 0, ui.WIDTH, 50, ui.BG)
for btn in self.pin_buttons: if self.subprompt:
btn.taint() display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG)
def render(self):
# pin matrix buttons
for btn in self.pin_buttons:
btn.render()
if not self.tainted:
return
# 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( display.text_center(
ui.WIDTH // 2, 46, self.sublabel, ui.NORMAL, ui.GREY, ui.BG ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG
) )
else: else:
# input line with header label display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG)
display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG)
self.tainted = False
def touch(self, event, pos): class PinButton(Button):
def __init__(self, index, digit, matrix):
self.matrix = matrix
super().__init__(digit_area(index), str(digit), ButtonMono)
def on_click(self):
self.matrix.assign(self.matrix.input.pin + self.content)
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:
self.cancel_button = Button(ui.grid(12), "")
self.cancel_button.disable()
self.pin_buttons = [
PinButton(i, d, self) for i, d in enumerate(generate_digits())
]
def dispatch(self, event, x, y):
for btn in self.pin_buttons: for btn in self.pin_buttons:
if btn.touch(event, pos) == BTN_CLICKED: btn.dispatch(event, x, y)
if len(self.pin) < self.maxlength: self.input.dispatch(event, x, y)
self.change(self.pin + btn.content) self.confirm_button.dispatch(event, x, y)
break if self.input.pin:
self.reset_button.dispatch(event, x, y)
else:
self.cancel_button.dispatch(event, x, y)
def change(self, pin): def assign(self, pin):
self.tainted = True if len(pin) > self.maxlength:
self.pin = pin return
for btn in self.pin_buttons: for btn in self.pin_buttons:
if len(self.pin) == self.maxlength: if len(pin) < self.maxlength:
btn.disable()
else:
btn.enable() btn.enable()
if self.onchange: else:
self.onchange() 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)

View File

@ -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)

View File

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

View File

@ -1,92 +1,180 @@
from micropython import const from micropython import const
from trezor import loop, res, ui 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 from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
if __debug__: if __debug__:
from apps.debug import swipe_signal from apps.debug import swipe_signal
async def change_page(page, page_count): def render_scrollbar(pages: int, page: int):
while True: BBOX = const(220)
if page == 0: SIZE = const(8)
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
else:
return result
async def animate_swipe():
time_delay = const(40000)
draw_delay = const(200000)
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
def render_scrollbar(page, page_count):
bbox = const(220)
size = const(8)
padding = 14 padding = 14
if page_count * padding > bbox: if pages * padding > BBOX:
padding = bbox // page_count padding = BBOX // pages
x = const(220) X = const(220)
y = (bbox // 2) - (page_count // 2) * padding Y = (BBOX // 2) - (pages // 2) * padding
for i in range(0, page_count): for i in range(0, pages):
if i != page: if i == page:
ui.display.bar_radius(x, y + i * padding, size, size, ui.GREY, ui.BG, 4) fg = ui.FG
ui.display.bar_radius(x, y + page * padding, size, size, ui.FG, ui.BG, 4) else:
fg = ui.GREY
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
class Scrollpage(ui.Widget): def render_swipe_icon():
def __init__(self, content, page, page_count): DRAW_DELAY = const(200000)
self.content = content
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)
def render_swipe_text():
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
class Paginated(ui.Layout):
def __init__(self, pages, page=0, one_by_one=False):
self.pages = pages
self.page = page self.page = page
self.page_count = page_count self.one_by_one = one_by_one
self.repaint = True
if content.__class__.__iter__ is not ui.Widget.__iter__: def dispatch(self, event, x, y):
raise TypeError( pages = self.pages
"Scrollpage does not support widgets with custom event loop" page = self.page
) pages[page].dispatch(event, x, y)
def taint(self): if event is ui.RENDER:
super().taint() length = len(pages)
self.content.taint() if page < length - 1:
render_swipe_icon()
if self.repaint:
render_swipe_text()
if self.repaint:
render_scrollbar(length, page)
self.repaint = False
def render(self): async def handle_paging(self):
self.content.render() if self.page == 0:
render_scrollbar(self.page, self.page_count) directions = SWIPE_UP
elif self.page == len(self.pages) - 1:
directions = SWIPE_DOWN
else:
directions = SWIPE_VERTICAL
def touch(self, event, pos): if __debug__:
return self.content.touch(event, pos) swipe = await loop.spawn(Swipe(directions), swipe_signal)
else:
swipe = await Swipe(directions)
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)
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
self.repaint = True
self.on_change()
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 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 on_right(self):
if self.index == self.count - 1:
self.paginated.on_confirm()
else:
self.paginated.on_up()
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)

View File

@ -1,6 +1,6 @@
from micropython import const from micropython import const
from trezor.ui import BOLD, MONO, NORMAL, rgb from trezor.ui import rgb
# radius for buttons and other elements # radius for buttons and other elements
RADIUS = const(2) RADIUS = const(2)
@ -66,193 +66,3 @@ ICON_BACK = "trezor/res/left.toig"
ICON_SWIPE = "trezor/res/swipe.toig" ICON_SWIPE = "trezor/res/swipe.toig"
ICON_CHECK = "trezor/res/check.toig" ICON_CHECK = "trezor/res/check.toig"
ICON_SPACE = "trezor/res/space.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,
},
}

View File

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

View File

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

View File

@ -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) class WordSelector(ui.Layout):
_W18 = const(18)
_W24 = const(24)
class WordSelector(Widget):
def __init__(self, content): def __init__(self, content):
self.content = content self.content = content
self.w12 = Button( self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12")
ui.grid(6, n_y=4, n_x=3, cells_y=2), str(_W12), style=ui.BTN_KEY 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 = Button( self.w18.on_click = self.on_w18
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), "24")
) self.w24.on_click = self.on_w24
self.w24 = Button(
ui.grid(8, n_y=4, n_x=3, cells_y=2), str(_W24), style=ui.BTN_KEY
)
def taint(self): def dispatch(self, event, x, y):
super().taint() self.content.dispatch(event, x, y)
self.w12.taint() self.w12.dispatch(event, x, y)
self.w18.taint() self.w18.dispatch(event, x, y)
self.w24.taint() self.w24.dispatch(event, x, y)
def render(self): def on_w12(self):
self.w12.render() raise ui.Result(12)
self.w18.render()
self.w24.render()
def touch(self, event, pos): def on_w18(self):
if self.w12.touch(event, pos) == BTN_CLICKED: raise ui.Result(18)
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): def on_w24(self):
if __debug__: raise ui.Result(24)
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)

View File

@ -18,10 +18,12 @@ if __debug__:
import uos import uos
TEST = int(uos.getenv("TREZOR_TEST") or "0") 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") SAVE_SCREEN = int(uos.getenv("TREZOR_SAVE_SCREEN") or "0")
LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0") LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0")
else: else:
TEST = 0 TEST = 0
DISABLE_FADE = 0
SAVE_SCREEN = 0 SAVE_SCREEN = 0
LOG_MEMORY = 0 LOG_MEMORY = 0

View File

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