You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
364 lines
11 KiB
364 lines
11 KiB
from typing import TYPE_CHECKING
|
|
|
|
from trezor import ui, utils, wire
|
|
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
|
|
from . import confirm_action
|
|
|
|
if TYPE_CHECKING:
|
|
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 = f"Recovery share #{share_index + 1}"
|
|
else:
|
|
header_title = f"Group {group_index + 1} - Share {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(f"{len(share_words)} words:")
|
|
text.br_half()
|
|
for index, word in first:
|
|
text.mono(f"{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(f"{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(f"{index + 1}. {word}")
|
|
shares_words_check.append(word)
|
|
text.br_half()
|
|
text.bold(f"I wrote down all {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
|
|
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 select_word(
|
|
ctx: wire.GenericContext,
|
|
words: Sequence[str],
|
|
share_index: int | None,
|
|
checked_index: int,
|
|
count: int,
|
|
group_index: int | None = None,
|
|
) -> str:
|
|
# let the user pick a word
|
|
select = MnemonicWordSelect(words, share_index, checked_index, count, group_index)
|
|
selected_word: str = await ctx.wait(select)
|
|
return selected_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 in (12, 20, 24):
|
|
middle = share[2:-2]
|
|
last = share[-2:] # two words on the last page
|
|
elif length in (18, 33):
|
|
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 += f"Set it to {count} and you will need "
|
|
if num_of_shares == 1:
|
|
text += "1 share."
|
|
elif num_of_shares == count:
|
|
text += f"all {count} of your {num_of_shares} shares."
|
|
else:
|
|
text += f"any {count} of your {num_of_shares} shares."
|
|
else:
|
|
text += "needed to form a group. "
|
|
text += f"Set it to {count} and you will "
|
|
if num_of_shares == 1:
|
|
text += "need 1 share "
|
|
elif num_of_shares == count:
|
|
text += f"need all {count} of {num_of_shares} shares "
|
|
else:
|
|
text += f"need any {count} of {num_of_shares} shares "
|
|
text += f"to form Group {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 "
|
|
f"Group {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
|
|
|
|
|
|
async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None:
|
|
if slip39:
|
|
description = "Never make a digital copy of your recovery shares and never upload them online!"
|
|
else:
|
|
description = "Never make a digital copy of your recovery seed and never upload\nit online!"
|
|
await confirm_action(
|
|
ctx,
|
|
"backup_warning",
|
|
"Caution",
|
|
description=description,
|
|
verb="I understand",
|
|
verb_cancel=None,
|
|
icon=ui.ICON_NOCOPY,
|
|
br_code=ButtonRequestType.ResetDevice,
|
|
)
|