mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-18 12:28:09 +00:00
refactor(core): convert apps.management.reset_device to layouts
This commit is contained in:
parent
574dcbc8a3
commit
312876ab67
@ -110,7 +110,8 @@ mypy:
|
|||||||
src/apps/bitcoin \
|
src/apps/bitcoin \
|
||||||
src/apps/cardano \
|
src/apps/cardano \
|
||||||
src/apps/misc \
|
src/apps/misc \
|
||||||
src/apps/webauthn
|
src/apps/webauthn \
|
||||||
|
src/trezor/ui
|
||||||
|
|
||||||
## code generation:
|
## code generation:
|
||||||
|
|
||||||
|
@ -523,7 +523,7 @@ if FROZEN:
|
|||||||
if TREZOR_MODEL == 'T':
|
if TREZOR_MODEL == 'T':
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py'))
|
||||||
elif TREZOR_MODEL == '1':
|
elif TREZOR_MODEL == '1':
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
||||||
|
@ -478,7 +478,7 @@ if FROZEN:
|
|||||||
if TREZOR_MODEL == 'T':
|
if TREZOR_MODEL == 'T':
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py'))
|
||||||
elif TREZOR_MODEL == '1':
|
elif TREZOR_MODEL == '1':
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/t1/*.py'))
|
||||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py'))
|
||||||
|
@ -88,7 +88,7 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success:
|
|||||||
secret, # for SLIP-39, this is the EMS
|
secret, # for SLIP-39, this is the EMS
|
||||||
msg.backup_type,
|
msg.backup_type,
|
||||||
needs_backup=not perform_backup,
|
needs_backup=not perform_backup,
|
||||||
no_backup=msg.no_backup,
|
no_backup=bool(msg.no_backup),
|
||||||
)
|
)
|
||||||
|
|
||||||
# if we backed up the wallet, show success message
|
# if we backed up the wallet, show success message
|
||||||
@ -109,12 +109,17 @@ async def backup_slip39_basic(
|
|||||||
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic)
|
await layout.slip39_show_checklist(ctx, 1, BackupType.Slip39_Basic)
|
||||||
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
|
threshold = await layout.slip39_prompt_threshold(ctx, shares_count)
|
||||||
|
|
||||||
|
identifier = storage.device.get_slip39_identifier()
|
||||||
|
iteration_exponent = storage.device.get_slip39_iteration_exponent()
|
||||||
|
if identifier is None or iteration_exponent is None:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
# generate the mnemonics
|
# generate the mnemonics
|
||||||
mnemonics = slip39.split_ems(
|
mnemonics = slip39.split_ems(
|
||||||
1, # Single Group threshold
|
1, # Single Group threshold
|
||||||
[(threshold, shares_count)], # Single Group threshold/count
|
[(threshold, shares_count)], # Single Group threshold/count
|
||||||
storage.device.get_slip39_identifier(),
|
identifier,
|
||||||
storage.device.get_slip39_iteration_exponent(),
|
iteration_exponent,
|
||||||
encrypted_master_secret,
|
encrypted_master_secret,
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
@ -144,12 +149,17 @@ async def backup_slip39_advanced(
|
|||||||
share_threshold = await layout.slip39_prompt_threshold(ctx, share_count, i)
|
share_threshold = await layout.slip39_prompt_threshold(ctx, share_count, i)
|
||||||
groups.append((share_threshold, share_count))
|
groups.append((share_threshold, share_count))
|
||||||
|
|
||||||
|
identifier = storage.device.get_slip39_identifier()
|
||||||
|
iteration_exponent = storage.device.get_slip39_iteration_exponent()
|
||||||
|
if identifier is None or iteration_exponent is None:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
# generate the mnemonics
|
# generate the mnemonics
|
||||||
mnemonics = slip39.split_ems(
|
mnemonics = slip39.split_ems(
|
||||||
group_threshold=group_threshold,
|
group_threshold=group_threshold,
|
||||||
groups=groups,
|
groups=groups,
|
||||||
identifier=storage.device.get_slip39_identifier(),
|
identifier=identifier,
|
||||||
iteration_exponent=storage.device.get_slip39_iteration_exponent(),
|
iteration_exponent=iteration_exponent,
|
||||||
encrypted_master_secret=encrypted_master_secret,
|
encrypted_master_secret=encrypted_master_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -193,7 +203,7 @@ def _compute_secret_from_entropy(
|
|||||||
|
|
||||||
async def backup_seed(
|
async def backup_seed(
|
||||||
ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
|
ctx: wire.Context, backup_type: BackupType, mnemonic_secret: bytes
|
||||||
):
|
) -> None:
|
||||||
if backup_type == BackupType.Slip39_Basic:
|
if backup_type == BackupType.Slip39_Basic:
|
||||||
await backup_slip39_basic(ctx, mnemonic_secret)
|
await backup_slip39_basic(ctx, mnemonic_secret)
|
||||||
elif backup_type == BackupType.Slip39_Advanced:
|
elif backup_type == BackupType.Slip39_Advanced:
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
from trezor import ui, utils
|
from trezor import ui, utils, wire
|
||||||
from trezor.crypto import random
|
from trezor.enums import ButtonRequestType
|
||||||
from trezor.enums import BackupType, ButtonRequestType
|
|
||||||
from trezor.ui.components.tt.button import Button, ButtonDefault
|
|
||||||
from trezor.ui.components.tt.checklist import Checklist
|
|
||||||
from trezor.ui.components.tt.info import InfoConfirm
|
|
||||||
from trezor.ui.components.tt.num_input import NumInput
|
|
||||||
from trezor.ui.components.tt.scroll import Paginated
|
|
||||||
from trezor.ui.components.tt.text import Text
|
|
||||||
from trezor.ui.layouts import confirm_action, confirm_blob, show_success, show_warning
|
from trezor.ui.layouts import confirm_action, confirm_blob, show_success, show_warning
|
||||||
|
from trezor.ui.layouts.tt.reset import ( # noqa: F401
|
||||||
from apps.common.confirm import confirm, require_hold_to_confirm
|
confirm_word,
|
||||||
|
show_share_words,
|
||||||
|
slip39_advanced_prompt_group_threshold,
|
||||||
|
slip39_advanced_prompt_number_of_groups,
|
||||||
|
slip39_prompt_number_of_shares,
|
||||||
|
slip39_prompt_threshold,
|
||||||
|
slip39_show_checklist,
|
||||||
|
)
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
from trezor import loop
|
from typing import Sequence
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
from apps import debug
|
|
||||||
|
|
||||||
|
|
||||||
async def show_internal_entropy(ctx, entropy: bytes):
|
async def show_internal_entropy(ctx: wire.GenericContext, entropy: bytes) -> None:
|
||||||
await confirm_blob(
|
await confirm_blob(
|
||||||
ctx,
|
ctx,
|
||||||
"entropy",
|
"entropy",
|
||||||
@ -30,90 +27,12 @@ async def show_internal_entropy(ctx, entropy: bytes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _show_share_words(ctx, share_words, share_index=None, group_index=None):
|
async def _confirm_share_words(
|
||||||
first, chunks, last = _split_share_into_pages(share_words)
|
ctx: wire.GenericContext,
|
||||||
|
share_index: int | None,
|
||||||
if share_index is None:
|
share_words: Sequence[str],
|
||||||
header_title = "Recovery seed"
|
group_index: int | None = None,
|
||||||
elif group_index is None:
|
) -> bool:
|
||||||
header_title = "Recovery share #%s" % (share_index + 1)
|
|
||||||
else:
|
|
||||||
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
|
|
||||||
header_icon = ui.ICON_RESET
|
|
||||||
pages = [] # ui page components
|
|
||||||
shares_words_check = [] # check we display correct data
|
|
||||||
|
|
||||||
# first page
|
|
||||||
text = Text(header_title, header_icon)
|
|
||||||
text.bold("Write down these")
|
|
||||||
text.bold("%s words:" % len(share_words))
|
|
||||||
text.br_half()
|
|
||||||
for index, word in first:
|
|
||||||
text.mono("%s. %s" % (index + 1, word))
|
|
||||||
shares_words_check.append(word)
|
|
||||||
pages.append(text)
|
|
||||||
|
|
||||||
# middle pages
|
|
||||||
for chunk in chunks:
|
|
||||||
text = Text(header_title, header_icon)
|
|
||||||
for index, word in chunk:
|
|
||||||
text.mono("%s. %s" % (index + 1, word))
|
|
||||||
shares_words_check.append(word)
|
|
||||||
pages.append(text)
|
|
||||||
|
|
||||||
# last page
|
|
||||||
text = Text(header_title, header_icon)
|
|
||||||
for index, word in last:
|
|
||||||
text.mono("%s. %s" % (index + 1, word))
|
|
||||||
shares_words_check.append(word)
|
|
||||||
text.br_half()
|
|
||||||
text.bold("I wrote down all %s" % len(share_words))
|
|
||||||
text.bold("words in order.")
|
|
||||||
pages.append(text)
|
|
||||||
|
|
||||||
# pagination
|
|
||||||
paginated = Paginated(pages)
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
|
|
||||||
word_pages = [first] + chunks + [last]
|
|
||||||
|
|
||||||
def export_displayed_words():
|
|
||||||
# export currently displayed mnemonic words into debuglink
|
|
||||||
words = [w for _, w in word_pages[paginated.page]]
|
|
||||||
debug.reset_current_words.publish(words)
|
|
||||||
|
|
||||||
paginated.on_change = export_displayed_words
|
|
||||||
export_displayed_words()
|
|
||||||
|
|
||||||
# make sure we display correct data
|
|
||||||
utils.ensure(share_words == shares_words_check)
|
|
||||||
|
|
||||||
# confirm the share
|
|
||||||
await require_hold_to_confirm(
|
|
||||||
ctx, paginated, ButtonRequestType.ResetDevice, cancel=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _split_share_into_pages(share_words):
|
|
||||||
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
|
||||||
first = share[:2] # two words on the first page
|
|
||||||
length = len(share_words)
|
|
||||||
if length == 12 or length == 20 or length == 24:
|
|
||||||
middle = share[2:-2]
|
|
||||||
last = share[-2:] # two words on the last page
|
|
||||||
elif length == 33 or length == 18:
|
|
||||||
middle = share[2:]
|
|
||||||
last = [] # no words at the last page, because it does not add up
|
|
||||||
else:
|
|
||||||
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
|
||||||
return first, list(chunks), last
|
|
||||||
|
|
||||||
|
|
||||||
async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
|
|
||||||
# divide list into thirds, rounding up, so that chunking by `third` always yields
|
# divide list into thirds, rounding up, so that chunking by `third` always yields
|
||||||
# three parts (the last one might be shorter)
|
# three parts (the last one might be shorter)
|
||||||
third = (len(share_words) + 2) // 3
|
third = (len(share_words) + 2) // 3
|
||||||
@ -121,41 +40,20 @@ async def _confirm_share_words(ctx, share_index, share_words, group_index=None):
|
|||||||
offset = 0
|
offset = 0
|
||||||
count = len(share_words)
|
count = len(share_words)
|
||||||
for part in utils.chunks(share_words, third):
|
for part in utils.chunks(share_words, third):
|
||||||
if not await _confirm_word(ctx, share_index, part, offset, count, group_index):
|
if not await confirm_word(ctx, share_index, part, offset, count, group_index):
|
||||||
return False
|
return False
|
||||||
offset += len(part)
|
offset += len(part)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _confirm_word(ctx, share_index, share_words, offset, count, group_index=None):
|
|
||||||
# remove duplicates
|
|
||||||
non_duplicates = list(set(share_words))
|
|
||||||
# shuffle list
|
|
||||||
random.shuffle(non_duplicates)
|
|
||||||
# take top NUM_OF_CHOICES words
|
|
||||||
choices = non_duplicates[: MnemonicWordSelect.NUM_OF_CHOICES]
|
|
||||||
# select first of them
|
|
||||||
checked_word = choices[0]
|
|
||||||
# find its index
|
|
||||||
checked_index = share_words.index(checked_word) + offset
|
|
||||||
# shuffle again so the confirmed word is not always the first choice
|
|
||||||
random.shuffle(choices)
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
debug.reset_word_index.publish(checked_index)
|
|
||||||
|
|
||||||
# let the user pick a word
|
|
||||||
select = MnemonicWordSelect(choices, share_index, checked_index, count, group_index)
|
|
||||||
selected_word = await ctx.wait(select)
|
|
||||||
# confirm it is the correct one
|
|
||||||
return selected_word == checked_word
|
|
||||||
|
|
||||||
|
|
||||||
async def _show_confirmation_success(
|
async def _show_confirmation_success(
|
||||||
ctx, share_index=None, num_of_shares=None, group_index=None
|
ctx: wire.GenericContext,
|
||||||
):
|
share_index: int | None = None,
|
||||||
if share_index is None: # it is a BIP39 backup
|
num_of_shares: int | None = None,
|
||||||
|
group_index: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
if share_index is None or num_of_shares is None: # it is a BIP39 backup
|
||||||
subheader = "You have finished\nverifying your\nrecovery seed."
|
subheader = "You have finished\nverifying your\nrecovery seed."
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
@ -183,7 +81,9 @@ async def _show_confirmation_success(
|
|||||||
return await show_success(ctx, "success_recovery", text, subheader=subheader)
|
return await show_success(ctx, "success_recovery", text, subheader=subheader)
|
||||||
|
|
||||||
|
|
||||||
async def _show_confirmation_failure(ctx, share_index):
|
async def _show_confirmation_failure(
|
||||||
|
ctx: wire.GenericContext, share_index: int | None
|
||||||
|
) -> None:
|
||||||
if share_index is None:
|
if share_index is None:
|
||||||
header = "Recovery seed"
|
header = "Recovery seed"
|
||||||
else:
|
else:
|
||||||
@ -199,7 +99,7 @@ async def _show_confirmation_failure(ctx, share_index):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def show_backup_warning(ctx, slip39=False):
|
async def show_backup_warning(ctx: wire.GenericContext, slip39: bool = False) -> None:
|
||||||
if slip39:
|
if slip39:
|
||||||
description = "Never make a digital copy of your recovery shares and never upload them online!"
|
description = "Never make a digital copy of your recovery shares and never upload them online!"
|
||||||
else:
|
else:
|
||||||
@ -216,7 +116,7 @@ async def show_backup_warning(ctx, slip39=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def show_backup_success(ctx):
|
async def show_backup_success(ctx: wire.GenericContext) -> None:
|
||||||
text = "Use your backup\nwhen you need to\nrecover your wallet."
|
text = "Use your backup\nwhen you need to\nrecover your wallet."
|
||||||
await show_success(ctx, "success_backup", text, subheader="Your backup is done.")
|
await show_success(ctx, "success_backup", text, subheader="Your backup is done.")
|
||||||
|
|
||||||
@ -225,7 +125,9 @@ async def show_backup_success(ctx):
|
|||||||
# ===
|
# ===
|
||||||
|
|
||||||
|
|
||||||
async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
async def bip39_show_and_confirm_mnemonic(
|
||||||
|
ctx: wire.GenericContext, mnemonic: str
|
||||||
|
) -> None:
|
||||||
# warn user about mnemonic safety
|
# warn user about mnemonic safety
|
||||||
await show_backup_warning(ctx)
|
await show_backup_warning(ctx)
|
||||||
|
|
||||||
@ -233,7 +135,7 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
# display paginated mnemonic on the screen
|
# display paginated mnemonic on the screen
|
||||||
await _show_share_words(ctx, share_words=words)
|
await show_share_words(ctx, share_words=words)
|
||||||
|
|
||||||
# make the user confirm some words from the mnemonic
|
# make the user confirm some words from the mnemonic
|
||||||
if await _confirm_share_words(ctx, None, words):
|
if await _confirm_share_words(ctx, None, words):
|
||||||
@ -247,119 +149,9 @@ async def bip39_show_and_confirm_mnemonic(ctx, mnemonic: str):
|
|||||||
# ===
|
# ===
|
||||||
|
|
||||||
|
|
||||||
async def slip39_show_checklist(ctx, step: int, backup_type: BackupType) -> None:
|
async def slip39_basic_show_and_confirm_shares(
|
||||||
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
ctx: wire.GenericContext, shares: Sequence[str]
|
||||||
if backup_type is BackupType.Slip39_Basic:
|
) -> None:
|
||||||
checklist.add("Set number of shares")
|
|
||||||
checklist.add("Set threshold")
|
|
||||||
checklist.add(("Write down and check", "all recovery shares"))
|
|
||||||
elif backup_type is BackupType.Slip39_Advanced:
|
|
||||||
checklist.add("Set number of groups")
|
|
||||||
checklist.add("Set group threshold")
|
|
||||||
checklist.add(("Set size and threshold", "for each group"))
|
|
||||||
checklist.select(step)
|
|
||||||
|
|
||||||
return await confirm(
|
|
||||||
ctx, checklist, ButtonRequestType.ResetDevice, cancel=None, confirm="Continue"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def slip39_prompt_threshold(ctx, num_of_shares, group_id=None):
|
|
||||||
count = num_of_shares // 2 + 1
|
|
||||||
# min value of share threshold is 2 unless the number of shares is 1
|
|
||||||
# number of shares 1 is possible in advnaced slip39
|
|
||||||
min_count = min(2, num_of_shares)
|
|
||||||
max_count = num_of_shares
|
|
||||||
|
|
||||||
while True:
|
|
||||||
shares = Slip39NumInput(
|
|
||||||
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
|
|
||||||
)
|
|
||||||
confirmed = await confirm(
|
|
||||||
ctx,
|
|
||||||
shares,
|
|
||||||
ButtonRequestType.ResetDevice,
|
|
||||||
cancel="Info",
|
|
||||||
confirm="Continue",
|
|
||||||
major_confirm=True,
|
|
||||||
cancel_style=ButtonDefault,
|
|
||||||
)
|
|
||||||
count = shares.input.count
|
|
||||||
if confirmed:
|
|
||||||
break
|
|
||||||
|
|
||||||
text = "The threshold sets the number of shares "
|
|
||||||
if group_id is None:
|
|
||||||
text += "needed to recover your wallet. "
|
|
||||||
text += "Set it to %s and you will need " % count
|
|
||||||
if num_of_shares == 1:
|
|
||||||
text += "1 share."
|
|
||||||
elif num_of_shares == count:
|
|
||||||
text += "all %s of your %s shares." % (count, num_of_shares)
|
|
||||||
else:
|
|
||||||
text += "any %s of your %s shares." % (count, num_of_shares)
|
|
||||||
else:
|
|
||||||
text += "needed to form a group. "
|
|
||||||
text += "Set it to %s and you will " % count
|
|
||||||
if num_of_shares == 1:
|
|
||||||
text += "need 1 share "
|
|
||||||
elif num_of_shares == count:
|
|
||||||
text += "need all %s of %s shares " % (count, num_of_shares)
|
|
||||||
else:
|
|
||||||
text += "need any %s of %s shares " % (count, num_of_shares)
|
|
||||||
text += "to form Group %s." % (group_id + 1)
|
|
||||||
info = InfoConfirm(text)
|
|
||||||
await info
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
async def slip39_prompt_number_of_shares(ctx, group_id=None):
|
|
||||||
count = 5
|
|
||||||
min_count = 1
|
|
||||||
max_count = 16
|
|
||||||
|
|
||||||
while True:
|
|
||||||
shares = Slip39NumInput(
|
|
||||||
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
|
|
||||||
)
|
|
||||||
confirmed = await confirm(
|
|
||||||
ctx,
|
|
||||||
shares,
|
|
||||||
ButtonRequestType.ResetDevice,
|
|
||||||
cancel="Info",
|
|
||||||
confirm="Continue",
|
|
||||||
major_confirm=True,
|
|
||||||
cancel_style=ButtonDefault,
|
|
||||||
)
|
|
||||||
count = shares.input.count
|
|
||||||
if confirmed:
|
|
||||||
break
|
|
||||||
|
|
||||||
if group_id is None:
|
|
||||||
info = InfoConfirm(
|
|
||||||
"Each recovery share is a "
|
|
||||||
"sequence of 20 words. "
|
|
||||||
"Next you will choose "
|
|
||||||
"how many shares you "
|
|
||||||
"need to recover your "
|
|
||||||
"wallet."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
info = InfoConfirm(
|
|
||||||
"Each recovery share is a "
|
|
||||||
"sequence of 20 words. "
|
|
||||||
"Next you will choose "
|
|
||||||
"the threshold number of "
|
|
||||||
"shares needed to form "
|
|
||||||
"Group %s." % (group_id + 1)
|
|
||||||
)
|
|
||||||
await info
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
async def slip39_basic_show_and_confirm_shares(ctx, shares):
|
|
||||||
# warn user about mnemonic safety
|
# warn user about mnemonic safety
|
||||||
await show_backup_warning(ctx, slip39=True)
|
await show_backup_warning(ctx, slip39=True)
|
||||||
|
|
||||||
@ -367,7 +159,7 @@ async def slip39_basic_show_and_confirm_shares(ctx, shares):
|
|||||||
share_words = share.split(" ")
|
share_words = share.split(" ")
|
||||||
while True:
|
while True:
|
||||||
# display paginated share on the screen
|
# display paginated share on the screen
|
||||||
await _show_share_words(ctx, share_words, index)
|
await show_share_words(ctx, share_words, index)
|
||||||
|
|
||||||
# make the user confirm words from the share
|
# make the user confirm words from the share
|
||||||
if await _confirm_share_words(ctx, index, share_words):
|
if await _confirm_share_words(ctx, index, share_words):
|
||||||
@ -379,73 +171,9 @@ async def slip39_basic_show_and_confirm_shares(ctx, shares):
|
|||||||
await _show_confirmation_failure(ctx, index)
|
await _show_confirmation_failure(ctx, index)
|
||||||
|
|
||||||
|
|
||||||
async def slip39_advanced_prompt_number_of_groups(ctx):
|
async def slip39_advanced_show_and_confirm_shares(
|
||||||
count = 5
|
ctx: wire.GenericContext, shares: Sequence[Sequence[str]]
|
||||||
min_count = 2
|
) -> None:
|
||||||
max_count = 16
|
|
||||||
|
|
||||||
while True:
|
|
||||||
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
|
|
||||||
confirmed = await confirm(
|
|
||||||
ctx,
|
|
||||||
shares,
|
|
||||||
ButtonRequestType.ResetDevice,
|
|
||||||
cancel="Info",
|
|
||||||
confirm="Continue",
|
|
||||||
major_confirm=True,
|
|
||||||
cancel_style=ButtonDefault,
|
|
||||||
)
|
|
||||||
count = shares.input.count
|
|
||||||
if confirmed:
|
|
||||||
break
|
|
||||||
|
|
||||||
info = InfoConfirm(
|
|
||||||
"Each group has a set "
|
|
||||||
"number of shares and "
|
|
||||||
"its own threshold. In the "
|
|
||||||
"next steps you will set "
|
|
||||||
"the numbers of shares "
|
|
||||||
"and the thresholds."
|
|
||||||
)
|
|
||||||
await info
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
async def slip39_advanced_prompt_group_threshold(ctx, num_of_groups):
|
|
||||||
count = num_of_groups // 2 + 1
|
|
||||||
min_count = 1
|
|
||||||
max_count = num_of_groups
|
|
||||||
|
|
||||||
while True:
|
|
||||||
shares = Slip39NumInput(
|
|
||||||
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
|
|
||||||
)
|
|
||||||
confirmed = await confirm(
|
|
||||||
ctx,
|
|
||||||
shares,
|
|
||||||
ButtonRequestType.ResetDevice,
|
|
||||||
cancel="Info",
|
|
||||||
confirm="Continue",
|
|
||||||
major_confirm=True,
|
|
||||||
cancel_style=ButtonDefault,
|
|
||||||
)
|
|
||||||
count = shares.input.count
|
|
||||||
if confirmed:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
info = InfoConfirm(
|
|
||||||
"The group threshold "
|
|
||||||
"specifies the number of "
|
|
||||||
"groups required to "
|
|
||||||
"recover your wallet. "
|
|
||||||
)
|
|
||||||
await info
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
async def slip39_advanced_show_and_confirm_shares(ctx, shares):
|
|
||||||
# warn user about mnemonic safety
|
# warn user about mnemonic safety
|
||||||
await show_backup_warning(ctx, slip39=True)
|
await show_backup_warning(ctx, slip39=True)
|
||||||
|
|
||||||
@ -454,7 +182,7 @@ async def slip39_advanced_show_and_confirm_shares(ctx, shares):
|
|||||||
share_words = share.split(" ")
|
share_words = share.split(" ")
|
||||||
while True:
|
while True:
|
||||||
# display paginated share on the screen
|
# display paginated share on the screen
|
||||||
await _show_share_words(ctx, share_words, share_index, group_index)
|
await show_share_words(ctx, share_words, share_index, group_index)
|
||||||
|
|
||||||
# make the user confirm words from the share
|
# make the user confirm words from the share
|
||||||
if await _confirm_share_words(
|
if await _confirm_share_words(
|
||||||
@ -469,131 +197,3 @@ async def slip39_advanced_show_and_confirm_shares(ctx, shares):
|
|||||||
break # this share is confirmed, go to next one
|
break # this share is confirmed, go to next one
|
||||||
else:
|
else:
|
||||||
await _show_confirmation_failure(ctx, share_index)
|
await _show_confirmation_failure(ctx, share_index)
|
||||||
|
|
||||||
|
|
||||||
class Slip39NumInput(ui.Component):
|
|
||||||
SET_SHARES = object()
|
|
||||||
SET_THRESHOLD = object()
|
|
||||||
SET_GROUPS = object()
|
|
||||||
SET_GROUP_THRESHOLD = object()
|
|
||||||
|
|
||||||
def __init__(self, step, count, min_count, max_count, group_id=None):
|
|
||||||
super().__init__()
|
|
||||||
self.step = step
|
|
||||||
self.input = NumInput(count, min_count=min_count, max_count=max_count)
|
|
||||||
self.input.on_change = self.on_change
|
|
||||||
self.group_id = group_id
|
|
||||||
|
|
||||||
def dispatch(self, event, x, y):
|
|
||||||
self.input.dispatch(event, x, y)
|
|
||||||
if event is ui.RENDER:
|
|
||||||
self.on_render()
|
|
||||||
|
|
||||||
def on_render(self):
|
|
||||||
if self.repaint:
|
|
||||||
count = self.input.count
|
|
||||||
|
|
||||||
# render the headline
|
|
||||||
if self.step is Slip39NumInput.SET_SHARES:
|
|
||||||
header = "Set num. of shares"
|
|
||||||
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
|
||||||
header = "Set threshold"
|
|
||||||
elif self.step is Slip39NumInput.SET_GROUPS:
|
|
||||||
header = "Set num. of groups"
|
|
||||||
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
|
||||||
header = "Set group threshold"
|
|
||||||
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
|
|
||||||
|
|
||||||
# render the counter
|
|
||||||
if self.step is Slip39NumInput.SET_SHARES:
|
|
||||||
if self.group_id is None:
|
|
||||||
if count == 1:
|
|
||||||
first_line_text = "Only one share will"
|
|
||||||
second_line_text = "be created."
|
|
||||||
else:
|
|
||||||
first_line_text = "%s people or locations" % count
|
|
||||||
second_line_text = "will each hold one share."
|
|
||||||
else:
|
|
||||||
first_line_text = "Set the total number of"
|
|
||||||
second_line_text = "shares in Group %s." % (self.group_id + 1)
|
|
||||||
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
|
||||||
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
|
|
||||||
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
|
|
||||||
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
|
||||||
if self.group_id is None:
|
|
||||||
first_line_text = "For recovery you need"
|
|
||||||
if count == 1:
|
|
||||||
second_line_text = "1 share."
|
|
||||||
elif count == self.input.max_count:
|
|
||||||
second_line_text = "all %s of the shares." % count
|
|
||||||
else:
|
|
||||||
second_line_text = "any %s of the shares." % count
|
|
||||||
else:
|
|
||||||
first_line_text = "The required number of "
|
|
||||||
second_line_text = "shares to form Group %s." % (self.group_id + 1)
|
|
||||||
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
|
||||||
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
|
|
||||||
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
|
|
||||||
elif self.step is Slip39NumInput.SET_GROUPS:
|
|
||||||
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
|
||||||
ui.display.text(
|
|
||||||
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
|
|
||||||
)
|
|
||||||
ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG)
|
|
||||||
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
|
||||||
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
|
||||||
ui.display.text(
|
|
||||||
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
|
|
||||||
)
|
|
||||||
ui.display.text(
|
|
||||||
12, 156, "groups for recovery.", ui.NORMAL, ui.FG, ui.BG
|
|
||||||
)
|
|
||||||
|
|
||||||
self.repaint = False
|
|
||||||
|
|
||||||
def on_change(self, count):
|
|
||||||
self.repaint = True
|
|
||||||
|
|
||||||
|
|
||||||
class MnemonicWordSelect(ui.Layout):
|
|
||||||
NUM_OF_CHOICES = 3
|
|
||||||
|
|
||||||
def __init__(self, words, share_index, word_index, count, group_index=None):
|
|
||||||
super().__init__()
|
|
||||||
self.words = words
|
|
||||||
self.share_index = share_index
|
|
||||||
self.word_index = word_index
|
|
||||||
self.buttons = []
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
area = ui.grid(i + 2, n_x=1)
|
|
||||||
btn = Button(area, word)
|
|
||||||
btn.on_click = self.select(word)
|
|
||||||
self.buttons.append(btn)
|
|
||||||
if share_index is None:
|
|
||||||
self.text = Text("Check seed")
|
|
||||||
elif group_index is None:
|
|
||||||
self.text = Text("Check share #%s" % (share_index + 1))
|
|
||||||
else:
|
|
||||||
self.text = Text(
|
|
||||||
"Check G%s - Share %s" % ((group_index + 1), (share_index + 1))
|
|
||||||
)
|
|
||||||
self.text.normal("Select word %d of %d:" % (word_index + 1, count))
|
|
||||||
|
|
||||||
def dispatch(self, event, x, y):
|
|
||||||
for btn in self.buttons:
|
|
||||||
btn.dispatch(event, x, y)
|
|
||||||
self.text.dispatch(event, x, y)
|
|
||||||
|
|
||||||
def select(self, word):
|
|
||||||
def fn():
|
|
||||||
raise ui.Result(word)
|
|
||||||
|
|
||||||
return fn
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
|
|
||||||
def read_content(self) -> list[str]:
|
|
||||||
return self.text.read_content() + [b.text for b in self.buttons]
|
|
||||||
|
|
||||||
def create_tasks(self) -> tuple[loop.Task, ...]:
|
|
||||||
return super().create_tasks() + (debug.input_signal(),)
|
|
||||||
|
@ -15,7 +15,7 @@ LINE_WIDTH = ui.WIDTH - TEXT_MARGIN_LEFT
|
|||||||
LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT
|
LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
from typing import Any, Union
|
from typing import Any, Sequence, Union
|
||||||
|
|
||||||
TextContent = Union[str, int]
|
TextContent = Union[str, int]
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ _WORKING_SPAN = Span()
|
|||||||
|
|
||||||
|
|
||||||
def render_text(
|
def render_text(
|
||||||
items: list[TextContent],
|
items: Sequence[TextContent],
|
||||||
new_lines: bool,
|
new_lines: bool,
|
||||||
max_lines: int,
|
max_lines: int,
|
||||||
font: int = ui.NORMAL,
|
font: int = ui.NORMAL,
|
||||||
|
154
core/src/trezor/ui/components/tt/reset.py
Normal file
154
core/src/trezor/ui/components/tt/reset.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
from trezor import ui
|
||||||
|
|
||||||
|
from .button import Button
|
||||||
|
from .num_input import NumInput
|
||||||
|
from .text import Text
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from trezor import loop
|
||||||
|
from typing import Callable, NoReturn, Sequence
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
from apps import debug
|
||||||
|
|
||||||
|
|
||||||
|
class Slip39NumInput(ui.Component):
|
||||||
|
SET_SHARES = object()
|
||||||
|
SET_THRESHOLD = object()
|
||||||
|
SET_GROUPS = object()
|
||||||
|
SET_GROUP_THRESHOLD = object()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
step: object,
|
||||||
|
count: int,
|
||||||
|
min_count: int,
|
||||||
|
max_count: int,
|
||||||
|
group_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.step = step
|
||||||
|
self.input = NumInput(count, min_count=min_count, max_count=max_count)
|
||||||
|
self.input.on_change = self.on_change # type: ignore
|
||||||
|
self.group_id = group_id
|
||||||
|
|
||||||
|
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||||
|
self.input.dispatch(event, x, y)
|
||||||
|
if event is ui.RENDER:
|
||||||
|
self.on_render()
|
||||||
|
|
||||||
|
def on_render(self) -> None:
|
||||||
|
if self.repaint:
|
||||||
|
count = self.input.count
|
||||||
|
|
||||||
|
# render the headline
|
||||||
|
if self.step is Slip39NumInput.SET_SHARES:
|
||||||
|
header = "Set num. of shares"
|
||||||
|
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
||||||
|
header = "Set threshold"
|
||||||
|
elif self.step is Slip39NumInput.SET_GROUPS:
|
||||||
|
header = "Set num. of groups"
|
||||||
|
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
||||||
|
header = "Set group threshold"
|
||||||
|
ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
|
||||||
|
|
||||||
|
# render the counter
|
||||||
|
if self.step is Slip39NumInput.SET_SHARES:
|
||||||
|
if self.group_id is None:
|
||||||
|
if count == 1:
|
||||||
|
first_line_text = "Only one share will"
|
||||||
|
second_line_text = "be created."
|
||||||
|
else:
|
||||||
|
first_line_text = "%s people or locations" % count
|
||||||
|
second_line_text = "will each hold one share."
|
||||||
|
else:
|
||||||
|
first_line_text = "Set the total number of"
|
||||||
|
second_line_text = "shares in Group %s." % (self.group_id + 1)
|
||||||
|
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
||||||
|
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
|
||||||
|
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
|
||||||
|
elif self.step is Slip39NumInput.SET_THRESHOLD:
|
||||||
|
if self.group_id is None:
|
||||||
|
first_line_text = "For recovery you need"
|
||||||
|
if count == 1:
|
||||||
|
second_line_text = "1 share."
|
||||||
|
elif count == self.input.max_count:
|
||||||
|
second_line_text = "all %s of the shares." % count
|
||||||
|
else:
|
||||||
|
second_line_text = "any %s of the shares." % count
|
||||||
|
else:
|
||||||
|
first_line_text = "The required number of "
|
||||||
|
second_line_text = "shares to form Group %s." % (self.group_id + 1)
|
||||||
|
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
||||||
|
ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG)
|
||||||
|
ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG)
|
||||||
|
elif self.step is Slip39NumInput.SET_GROUPS:
|
||||||
|
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
||||||
|
ui.display.text(
|
||||||
|
12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG)
|
||||||
|
elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD:
|
||||||
|
ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG)
|
||||||
|
ui.display.text(
|
||||||
|
12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
ui.display.text(
|
||||||
|
12, 156, "groups for recovery.", ui.NORMAL, ui.FG, ui.BG
|
||||||
|
)
|
||||||
|
|
||||||
|
self.repaint = False
|
||||||
|
|
||||||
|
def on_change(self, count: int) -> None:
|
||||||
|
self.repaint = True
|
||||||
|
|
||||||
|
|
||||||
|
class MnemonicWordSelect(ui.Layout):
|
||||||
|
NUM_OF_CHOICES = 3
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
words: Sequence[str],
|
||||||
|
share_index: int | None,
|
||||||
|
word_index: int,
|
||||||
|
count: int,
|
||||||
|
group_index: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.words = words
|
||||||
|
self.share_index = share_index
|
||||||
|
self.word_index = word_index
|
||||||
|
self.buttons = []
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
area = ui.grid(i + 2, n_x=1)
|
||||||
|
btn = Button(area, word)
|
||||||
|
btn.on_click = self.select(word) # type: ignore
|
||||||
|
self.buttons.append(btn)
|
||||||
|
if share_index is None:
|
||||||
|
self.text: ui.Component = Text("Check seed")
|
||||||
|
elif group_index is None:
|
||||||
|
self.text = Text("Check share #%s" % (share_index + 1))
|
||||||
|
else:
|
||||||
|
self.text = Text(
|
||||||
|
"Check G%s - Share %s" % ((group_index + 1), (share_index + 1))
|
||||||
|
)
|
||||||
|
self.text.normal("Select word %d of %d:" % (word_index + 1, count))
|
||||||
|
|
||||||
|
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||||
|
for btn in self.buttons:
|
||||||
|
btn.dispatch(event, x, y)
|
||||||
|
self.text.dispatch(event, x, y)
|
||||||
|
|
||||||
|
def select(self, word: str) -> Callable:
|
||||||
|
def fn() -> NoReturn:
|
||||||
|
raise ui.Result(word)
|
||||||
|
|
||||||
|
return fn
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
|
||||||
|
def read_content(self) -> list[str]:
|
||||||
|
return self.text.read_content() + [b.text for b in self.buttons]
|
||||||
|
|
||||||
|
def create_tasks(self) -> tuple[loop.Task, ...]:
|
||||||
|
return super().create_tasks() + (debug.input_signal(),)
|
@ -9,21 +9,21 @@ from trezor.ui.popup import Popup
|
|||||||
from trezor.ui.qr import Qr
|
from trezor.ui.qr import Qr
|
||||||
from trezor.utils import chunks, chunks_intersperse
|
from trezor.utils import chunks, chunks_intersperse
|
||||||
|
|
||||||
from ..components.common import break_path_to_lines
|
from ...components.common import break_path_to_lines
|
||||||
from ..components.common.confirm import is_confirmed, raise_if_cancelled
|
from ...components.common.confirm import is_confirmed, raise_if_cancelled
|
||||||
from ..components.common.webauthn import ConfirmInfo
|
from ...components.common.webauthn import ConfirmInfo
|
||||||
from ..components.tt import passphrase, pin
|
from ...components.tt import passphrase, pin
|
||||||
from ..components.tt.button import ButtonCancel, ButtonDefault
|
from ...components.tt.button import ButtonCancel, ButtonDefault
|
||||||
from ..components.tt.confirm import Confirm, ConfirmPageable, HoldToConfirm, Pageable
|
from ...components.tt.confirm import Confirm, ConfirmPageable, HoldToConfirm, Pageable
|
||||||
from ..components.tt.scroll import (
|
from ...components.tt.scroll import (
|
||||||
PAGEBREAK,
|
PAGEBREAK,
|
||||||
Paginated,
|
Paginated,
|
||||||
paginate_paragraphs,
|
paginate_paragraphs,
|
||||||
paginate_text,
|
paginate_text,
|
||||||
)
|
)
|
||||||
from ..components.tt.text import LINE_WIDTH_PAGINATED, Span, Text
|
from ...components.tt.text import LINE_WIDTH_PAGINATED, Span, Text
|
||||||
from ..components.tt.webauthn import ConfirmContent
|
from ...components.tt.webauthn import ConfirmContent
|
||||||
from ..constants.tt import (
|
from ...constants.tt import (
|
||||||
MONO_ADDR_PER_LINE,
|
MONO_ADDR_PER_LINE,
|
||||||
MONO_HEX_PER_LINE,
|
MONO_HEX_PER_LINE,
|
||||||
QR_SIZE_THRESHOLD,
|
QR_SIZE_THRESHOLD,
|
||||||
@ -31,7 +31,7 @@ from ..constants.tt import (
|
|||||||
QR_Y,
|
QR_Y,
|
||||||
TEXT_MAX_LINES,
|
TEXT_MAX_LINES,
|
||||||
)
|
)
|
||||||
from .common import button_request, interact
|
from ..common import button_request, interact
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
from typing import (
|
from typing import (
|
361
core/src/trezor/ui/layouts/tt/reset.py
Normal file
361
core/src/trezor/ui/layouts/tt/reset.py
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
from trezor import ui, utils, wire
|
||||||
|
from trezor.crypto import random
|
||||||
|
from trezor.enums import BackupType, ButtonRequestType
|
||||||
|
|
||||||
|
from ...components.common.confirm import is_confirmed, raise_if_cancelled
|
||||||
|
from ...components.tt.button import ButtonDefault
|
||||||
|
from ...components.tt.checklist import Checklist
|
||||||
|
from ...components.tt.confirm import Confirm, HoldToConfirm
|
||||||
|
from ...components.tt.info import InfoConfirm
|
||||||
|
from ...components.tt.reset import MnemonicWordSelect, Slip39NumInput
|
||||||
|
from ...components.tt.scroll import Paginated
|
||||||
|
from ...components.tt.text import Text
|
||||||
|
from ..common import interact
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
NumberedWords = Sequence[tuple[int, str]]
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
from apps import debug
|
||||||
|
|
||||||
|
|
||||||
|
async def show_share_words(
|
||||||
|
ctx: wire.GenericContext,
|
||||||
|
share_words: Sequence[str],
|
||||||
|
share_index: int | None = None,
|
||||||
|
group_index: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
first, chunks, last = _split_share_into_pages(share_words)
|
||||||
|
|
||||||
|
if share_index is None:
|
||||||
|
header_title = "Recovery seed"
|
||||||
|
elif group_index is None:
|
||||||
|
header_title = "Recovery share #%s" % (share_index + 1)
|
||||||
|
else:
|
||||||
|
header_title = "Group %s - Share %s" % ((group_index + 1), (share_index + 1))
|
||||||
|
header_icon = ui.ICON_RESET
|
||||||
|
pages: list[ui.Component] = [] # ui page components
|
||||||
|
shares_words_check = [] # check we display correct data
|
||||||
|
|
||||||
|
# first page
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
text.bold("Write down these")
|
||||||
|
text.bold("%s words:" % len(share_words))
|
||||||
|
text.br_half()
|
||||||
|
for index, word in first:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
# middle pages
|
||||||
|
for chunk in chunks:
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
for index, word in chunk:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
# last page
|
||||||
|
text = Text(header_title, header_icon)
|
||||||
|
for index, word in last:
|
||||||
|
text.mono("%s. %s" % (index + 1, word))
|
||||||
|
shares_words_check.append(word)
|
||||||
|
text.br_half()
|
||||||
|
text.bold("I wrote down all %s" % len(share_words))
|
||||||
|
text.bold("words in order.")
|
||||||
|
pages.append(text)
|
||||||
|
|
||||||
|
pages[-1] = HoldToConfirm(pages[-1], cancel=False)
|
||||||
|
|
||||||
|
# pagination
|
||||||
|
paginated = Paginated(pages)
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
|
||||||
|
word_pages = [first] + chunks + [last]
|
||||||
|
|
||||||
|
def export_displayed_words() -> None:
|
||||||
|
# export currently displayed mnemonic words into debuglink
|
||||||
|
words = [w for _, w in word_pages[paginated.page]]
|
||||||
|
debug.reset_current_words.publish(words)
|
||||||
|
|
||||||
|
paginated.on_change = export_displayed_words # type: ignore
|
||||||
|
export_displayed_words()
|
||||||
|
|
||||||
|
# make sure we display correct data
|
||||||
|
utils.ensure(share_words == shares_words_check)
|
||||||
|
|
||||||
|
# confirm the share
|
||||||
|
await raise_if_cancelled(
|
||||||
|
interact(
|
||||||
|
ctx,
|
||||||
|
paginated,
|
||||||
|
"backup_words",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_word(
|
||||||
|
ctx: wire.GenericContext,
|
||||||
|
share_index: int | None,
|
||||||
|
share_words: Sequence[str],
|
||||||
|
offset: int,
|
||||||
|
count: int,
|
||||||
|
group_index: int | None = None,
|
||||||
|
) -> bool:
|
||||||
|
# remove duplicates
|
||||||
|
non_duplicates = list(set(share_words))
|
||||||
|
# shuffle list
|
||||||
|
random.shuffle(non_duplicates)
|
||||||
|
# take top NUM_OF_CHOICES words
|
||||||
|
choices = non_duplicates[: MnemonicWordSelect.NUM_OF_CHOICES]
|
||||||
|
# select first of them
|
||||||
|
checked_word = choices[0]
|
||||||
|
# find its index
|
||||||
|
checked_index = share_words.index(checked_word) + offset
|
||||||
|
# shuffle again so the confirmed word is not always the first choice
|
||||||
|
random.shuffle(choices)
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
debug.reset_word_index.publish(checked_index)
|
||||||
|
|
||||||
|
# let the user pick a word
|
||||||
|
select = MnemonicWordSelect(choices, share_index, checked_index, count, group_index)
|
||||||
|
selected_word: str = await ctx.wait(select)
|
||||||
|
# confirm it is the correct one
|
||||||
|
return selected_word == checked_word
|
||||||
|
|
||||||
|
|
||||||
|
def _split_share_into_pages(
|
||||||
|
share_words: Sequence[str],
|
||||||
|
) -> tuple[NumberedWords, list[NumberedWords], NumberedWords]:
|
||||||
|
share = list(enumerate(share_words)) # we need to keep track of the word indices
|
||||||
|
first = share[:2] # two words on the first page
|
||||||
|
length = len(share_words)
|
||||||
|
if length == 12 or length == 20 or length == 24:
|
||||||
|
middle = share[2:-2]
|
||||||
|
last = share[-2:] # two words on the last page
|
||||||
|
elif length == 33 or length == 18:
|
||||||
|
middle = share[2:]
|
||||||
|
last = [] # no words at the last page, because it does not add up
|
||||||
|
else:
|
||||||
|
# Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
chunks = utils.chunks(middle, 4) # 4 words on the middle pages
|
||||||
|
return first, list(chunks), last
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_show_checklist(
|
||||||
|
ctx: wire.GenericContext, step: int, backup_type: BackupType
|
||||||
|
) -> None:
|
||||||
|
checklist = Checklist("Backup checklist", ui.ICON_RESET)
|
||||||
|
if backup_type is BackupType.Slip39_Basic:
|
||||||
|
checklist.add("Set number of shares")
|
||||||
|
checklist.add("Set threshold")
|
||||||
|
checklist.add(("Write down and check", "all recovery shares"))
|
||||||
|
elif backup_type is BackupType.Slip39_Advanced:
|
||||||
|
checklist.add("Set number of groups")
|
||||||
|
checklist.add("Set group threshold")
|
||||||
|
checklist.add(("Set size and threshold", "for each group"))
|
||||||
|
checklist.select(step)
|
||||||
|
|
||||||
|
await raise_if_cancelled(
|
||||||
|
interact(
|
||||||
|
ctx,
|
||||||
|
Confirm(checklist, confirm="Continue", cancel=None),
|
||||||
|
"slip39_checklist",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_prompt_threshold(
|
||||||
|
ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None
|
||||||
|
) -> int:
|
||||||
|
count = num_of_shares // 2 + 1
|
||||||
|
# min value of share threshold is 2 unless the number of shares is 1
|
||||||
|
# number of shares 1 is possible in advnaced slip39
|
||||||
|
min_count = min(2, num_of_shares)
|
||||||
|
max_count = num_of_shares
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = Slip39NumInput(
|
||||||
|
Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id
|
||||||
|
)
|
||||||
|
confirmed = is_confirmed(
|
||||||
|
await interact(
|
||||||
|
ctx,
|
||||||
|
Confirm(
|
||||||
|
shares,
|
||||||
|
confirm="Continue",
|
||||||
|
cancel="Info",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
),
|
||||||
|
"slip39_threshold",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
|
||||||
|
text = "The threshold sets the number of shares "
|
||||||
|
if group_id is None:
|
||||||
|
text += "needed to recover your wallet. "
|
||||||
|
text += "Set it to %s and you will need " % count
|
||||||
|
if num_of_shares == 1:
|
||||||
|
text += "1 share."
|
||||||
|
elif num_of_shares == count:
|
||||||
|
text += "all %s of your %s shares." % (count, num_of_shares)
|
||||||
|
else:
|
||||||
|
text += "any %s of your %s shares." % (count, num_of_shares)
|
||||||
|
else:
|
||||||
|
text += "needed to form a group. "
|
||||||
|
text += "Set it to %s and you will " % count
|
||||||
|
if num_of_shares == 1:
|
||||||
|
text += "need 1 share "
|
||||||
|
elif num_of_shares == count:
|
||||||
|
text += "need all %s of %s shares " % (count, num_of_shares)
|
||||||
|
else:
|
||||||
|
text += "need any %s of %s shares " % (count, num_of_shares)
|
||||||
|
text += "to form Group %s." % (group_id + 1)
|
||||||
|
info = InfoConfirm(text)
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_prompt_number_of_shares(
|
||||||
|
ctx: wire.GenericContext, group_id: int | None = None
|
||||||
|
) -> int:
|
||||||
|
count = 5
|
||||||
|
min_count = 1
|
||||||
|
max_count = 16
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = Slip39NumInput(
|
||||||
|
Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id
|
||||||
|
)
|
||||||
|
confirmed = is_confirmed(
|
||||||
|
await interact(
|
||||||
|
ctx,
|
||||||
|
Confirm(
|
||||||
|
shares,
|
||||||
|
confirm="Continue",
|
||||||
|
cancel="Info",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
),
|
||||||
|
"slip39_shares",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
|
||||||
|
if group_id is None:
|
||||||
|
info = InfoConfirm(
|
||||||
|
"Each recovery share is a "
|
||||||
|
"sequence of 20 words. "
|
||||||
|
"Next you will choose "
|
||||||
|
"how many shares you "
|
||||||
|
"need to recover your "
|
||||||
|
"wallet."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
info = InfoConfirm(
|
||||||
|
"Each recovery share is a "
|
||||||
|
"sequence of 20 words. "
|
||||||
|
"Next you will choose "
|
||||||
|
"the threshold number of "
|
||||||
|
"shares needed to form "
|
||||||
|
"Group %s." % (group_id + 1)
|
||||||
|
)
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int:
|
||||||
|
count = 5
|
||||||
|
min_count = 2
|
||||||
|
max_count = 16
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count)
|
||||||
|
confirmed = is_confirmed(
|
||||||
|
await interact(
|
||||||
|
ctx,
|
||||||
|
Confirm(
|
||||||
|
shares,
|
||||||
|
confirm="Continue",
|
||||||
|
cancel="Info",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
),
|
||||||
|
"slip39_groups",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
|
||||||
|
info = InfoConfirm(
|
||||||
|
"Each group has a set "
|
||||||
|
"number of shares and "
|
||||||
|
"its own threshold. In the "
|
||||||
|
"next steps you will set "
|
||||||
|
"the numbers of shares "
|
||||||
|
"and the thresholds."
|
||||||
|
)
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def slip39_advanced_prompt_group_threshold(
|
||||||
|
ctx: wire.GenericContext, num_of_groups: int
|
||||||
|
) -> int:
|
||||||
|
count = num_of_groups // 2 + 1
|
||||||
|
min_count = 1
|
||||||
|
max_count = num_of_groups
|
||||||
|
|
||||||
|
while True:
|
||||||
|
shares = Slip39NumInput(
|
||||||
|
Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count
|
||||||
|
)
|
||||||
|
confirmed = is_confirmed(
|
||||||
|
await interact(
|
||||||
|
ctx,
|
||||||
|
Confirm(
|
||||||
|
shares,
|
||||||
|
confirm="Continue",
|
||||||
|
cancel="Info",
|
||||||
|
major_confirm=True,
|
||||||
|
cancel_style=ButtonDefault,
|
||||||
|
),
|
||||||
|
"slip39_group_threshold",
|
||||||
|
ButtonRequestType.ResetDevice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
count = shares.input.count
|
||||||
|
if confirmed:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
info = InfoConfirm(
|
||||||
|
"The group threshold "
|
||||||
|
"specifies the number of "
|
||||||
|
"groups required to "
|
||||||
|
"recover your wallet. "
|
||||||
|
)
|
||||||
|
await info
|
||||||
|
|
||||||
|
return count
|
Loading…
Reference in New Issue
Block a user