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:
parent
2f4c123466
commit
36534325f0
@ -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()
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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 ""
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
|
||||||
|
@ -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?")
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
from micropython import const
|
|
||||||
|
|
||||||
from trezor import loop, ui
|
|
||||||
from trezor.ui import Widget
|
|
||||||
from trezor.ui.button import BTN_CLICKED, Button
|
|
||||||
|
|
||||||
DEVICE = const(0)
|
|
||||||
HOST = const(1)
|
|
||||||
|
|
||||||
|
|
||||||
class EntrySelector(Widget):
|
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
|
|
||||||
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
|
|
||||||
|
|
||||||
def taint(self):
|
|
||||||
super().taint()
|
|
||||||
self.device.taint()
|
|
||||||
self.host.taint()
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
self.device.render()
|
|
||||||
self.host.render()
|
|
||||||
|
|
||||||
def touch(self, event, pos):
|
|
||||||
if self.device.touch(event, pos) == BTN_CLICKED:
|
|
||||||
return DEVICE
|
|
||||||
if self.host.touch(event, pos) == BTN_CLICKED:
|
|
||||||
return HOST
|
|
||||||
|
|
||||||
async def __iter__(self):
|
|
||||||
return await loop.spawn(super().__iter__(), self.content)
|
|
@ -1,36 +1,57 @@
|
|||||||
import utime
|
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
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
17
core/src/trezor/ui/popup.py
Normal file
17
core/src/trezor/ui/popup.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user