1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-19 12:58:13 +00:00

fix(core/mercury): show remaining shares

This commit enables showing "Remaining shares" from the context menu
during recovery process. This is only shown during super-shamir.
This commit is contained in:
obrusvit 2024-08-26 17:10:04 +02:00 committed by Vít Obrusník
parent 708b0274f5
commit c5f9fadbd1
12 changed files with 314 additions and 200 deletions

View File

@ -0,0 +1 @@
[T3T1] Added missing info about remaining shares in super-shamir recovery.

View File

@ -1,6 +1,6 @@
use crate::{
error,
micropython::{map::Map, obj::Obj, qstr::Qstr, util},
micropython::{iter::IterBuf, map::Map, obj::Obj, qstr::Qstr, util},
strutil::TString,
translations::TR,
ui::{
@ -8,12 +8,14 @@ use crate::{
component::{
button_request::ButtonRequestExt,
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
text::paragraphs::{
Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, VecExt,
},
ComponentExt, SwipeDirection,
},
flow::{
base::{DecisionBuilder as _, StateChange},
FlowMsg, FlowState, SwipeFlow,
FlowMsg, FlowState, SwipeFlow, SwipePage,
},
layout::{obj::LayoutObj, util::RecoveryType},
},
@ -40,6 +42,15 @@ pub enum ContinueRecoveryBetweenShares {
CancelConfirm,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ContinueRecoveryBetweenSharesAdvanced {
Main,
Menu,
CancelIntro,
CancelConfirm,
RemainingShares,
}
impl FlowState for ContinueRecoveryBeforeShares {
#[inline]
fn index(&'static self) -> usize {
@ -96,56 +107,99 @@ impl FlowState for ContinueRecoveryBetweenShares {
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_continue_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe {
util::try_with_args_and_kwargs(n_args, args, kwargs, ContinueRecoveryBeforeShares::new_obj)
impl FlowState for ContinueRecoveryBetweenSharesAdvanced {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
match (self, direction) {
(Self::Main, SwipeDirection::Left) => Self::Menu.swipe(direction),
(Self::Menu, SwipeDirection::Right) => Self::Main.swipe(direction),
(Self::Main, SwipeDirection::Up) => self.return_msg(FlowMsg::Confirmed),
(Self::CancelIntro, SwipeDirection::Up) => Self::CancelConfirm.swipe(direction),
(Self::CancelIntro, SwipeDirection::Right) => Self::Menu.swipe(direction),
(Self::CancelConfirm, SwipeDirection::Down) => Self::CancelIntro.swipe(direction),
(Self::RemainingShares, SwipeDirection::Right) => Self::Menu.swipe(direction),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
match (self, msg) {
(Self::Main, FlowMsg::Info) => Self::Menu.transit(),
(Self::Menu, FlowMsg::Choice(0)) => Self::RemainingShares.transit(),
(Self::Menu, FlowMsg::Choice(1)) => Self::CancelIntro.swipe_left(),
(Self::Menu, FlowMsg::Cancelled) => Self::Main.swipe_right(),
(Self::CancelIntro, FlowMsg::Cancelled) => Self::Menu.transit(),
(Self::CancelConfirm, FlowMsg::Cancelled) => Self::CancelIntro.swipe_right(),
(Self::CancelConfirm, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Cancelled),
(Self::RemainingShares, FlowMsg::Cancelled) => Self::Menu.transit(),
_ => self.do_nothing(),
}
}
}
impl ContinueRecoveryBeforeShares {
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let first_screen: bool = kwargs.get(Qstr::MP_QSTR_first_screen)?.try_into()?;
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
let text: TString = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; // #shares entered
let subtext: Option<TString> = kwargs.get(Qstr::MP_QSTR_subtext)?.try_into_option()?; // #shares remaining
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_continue_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, new_obj) }
}
let (title, cancel_btn, cancel_title, cancel_intro) = match recovery_type {
RecoveryType::Normal => (
TR::recovery__title,
TR::recovery__title_cancel_recovery,
TR::recovery__title_cancel_recovery,
TR::recovery__wanna_cancel_recovery,
),
_ => (
TR::recovery__title_dry_run,
TR::recovery__cancel_dry_run,
TR::recovery__title_cancel_dry_run,
TR::recovery__wanna_cancel_dry_run,
),
};
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let first_screen: bool = kwargs.get(Qstr::MP_QSTR_first_screen)?.try_into()?;
let recovery_type: RecoveryType = kwargs.get(Qstr::MP_QSTR_recovery_type)?.try_into()?;
let text: TString = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; // #shares entered
let subtext: Option<TString> = kwargs.get(Qstr::MP_QSTR_subtext)?.try_into_option()?; // #shares remaining
let pages: Option<Obj> = kwargs.get(Qstr::MP_QSTR_pages)?.try_into_option()?; // info about remaining shares
let mut pars = ParagraphVecShort::new();
let footer_instruction;
let footer_description;
if first_screen {
pars.add(Paragraph::new(
&theme::TEXT_MAIN_GREY_EXTRA_LIGHT,
TR::recovery__enter_each_word,
));
footer_instruction = TR::instructions__swipe_up.into();
footer_description = None;
} else {
pars.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
if let Some(sub) = subtext {
pars.add(Paragraph::new(&theme::TEXT_SUB_GREY, sub));
}
footer_instruction = TR::instructions__swipe_up.into();
footer_description = Some(TR::instructions__enter_next_share.into());
let mut pars_show_shares = ParagraphVecLong::new();
if let Some(pages) = pages {
let pages_iterable: Obj = pages;
for page in IterBuf::new().try_iterate(pages_iterable)? {
let [title, description]: [TString; 2] = util::iter_into_array(page)?;
pars_show_shares
.add(Paragraph::new(&theme::TEXT_SUB_GREY, title))
.add(Paragraph::new(&theme::TEXT_MONO_GREY_LIGHT, description).break_after());
}
}
let paragraphs_main = Paragraphs::new(pars);
let content_main = Frame::left_aligned(title.into(), SwipeContent::new(paragraphs_main))
let (title, cancel_btn, cancel_title, cancel_intro) = match recovery_type {
RecoveryType::Normal => (
TR::recovery__title,
TR::recovery__title_cancel_recovery,
TR::recovery__title_cancel_recovery,
TR::recovery__wanna_cancel_recovery,
),
_ => (
TR::recovery__title_dry_run,
TR::recovery__cancel_dry_run,
TR::recovery__title_cancel_dry_run,
TR::recovery__wanna_cancel_dry_run,
),
};
let mut pars_main = ParagraphVecShort::new();
let footer_instruction;
let footer_description;
if first_screen {
pars_main.add(Paragraph::new(
&theme::TEXT_MAIN_GREY_EXTRA_LIGHT,
TR::recovery__enter_each_word,
));
footer_instruction = TR::instructions__swipe_up.into();
footer_description = None;
} else {
pars_main.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
if let Some(sub) = subtext {
pars_main.add(Paragraph::new(&theme::TEXT_SUB_GREY, sub));
}
footer_instruction = TR::instructions__swipe_up.into();
footer_description = Some(TR::instructions__enter_next_share.into());
}
let content_main =
Frame::left_aligned(title.into(), SwipeContent::new(pars_main.into_paragraphs()))
.with_subtitle(TR::words__instructions.into())
.with_menu_button()
.with_footer(footer_instruction, footer_description)
@ -155,8 +209,47 @@ impl ContinueRecoveryBeforeShares {
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::RecoveryHomepage,
"recovery".into(),
))
.with_pages(|_| 1);
let paragraphs_cancel = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, cancel_intro).with_bottom_padding(17),
Paragraph::new(&theme::TEXT_WARNING, TR::recovery__progress_will_be_lost),
])
.into_paragraphs();
let content_cancel_intro =
Frame::left_aligned(cancel_title.into(), SwipeContent::new(paragraphs_cancel))
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::words__continue_anyway.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(FlowMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
})
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::ProtectCall,
"abort_recovery".into(),
));
let content_cancel_confirm = Frame::left_aligned(
cancel_title.into(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(PromptMsg::Confirmed) => Some(FlowMsg::Confirmed),
FrameMsg::Button(FlowMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
});
let res = if first_screen {
let content_menu = Frame::left_aligned(
TString::empty(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, cancel_btn.into()),
@ -168,60 +261,81 @@ impl ContinueRecoveryBeforeShares {
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let paragraphs_cancel = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, cancel_intro).with_bottom_padding(17),
Paragraph::new(&theme::TEXT_WARNING, TR::recovery__progress_will_be_lost),
])
.into_paragraphs();
let content_cancel_intro =
Frame::left_aligned(cancel_title.into(), SwipeContent::new(paragraphs_cancel))
.with_cancel_button()
.with_footer(
TR::instructions__swipe_up.into(),
Some(TR::words__continue_anyway.into()),
)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Button(FlowMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
})
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::ProtectCall,
"abort_recovery".into(),
));
let content_cancel_confirm = Frame::left_aligned(
cancel_title.into(),
SwipeContent::new(PromptScreen::new_tap_to_cancel()),
SwipeFlow::new(&ContinueRecoveryBeforeShares::Main)?
.with_page(&ContinueRecoveryBeforeShares::Main, content_main)?
.with_page(&ContinueRecoveryBeforeShares::Menu, content_menu)?
} else if pars_show_shares.is_empty() {
let content_menu = Frame::left_aligned(
TString::empty(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, cancel_btn.into()),
)
.with_cancel_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(PromptMsg::Confirmed) => Some(FlowMsg::Confirmed),
FrameMsg::Button(FlowMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let res = if first_screen {
SwipeFlow::new(&ContinueRecoveryBeforeShares::Main)?
.with_page(&ContinueRecoveryBeforeShares::Main, content_main)?
.with_page(&ContinueRecoveryBeforeShares::Menu, content_menu)?
} else {
SwipeFlow::new(&ContinueRecoveryBetweenShares::Main)?
.with_page(&ContinueRecoveryBetweenShares::Main, content_main)?
.with_page(&ContinueRecoveryBetweenShares::Menu, content_menu)?
.with_page(
&ContinueRecoveryBetweenShares::CancelIntro,
content_cancel_intro,
)?
.with_page(
&ContinueRecoveryBetweenShares::CancelConfirm,
content_cancel_confirm,
)?
};
Ok(LayoutObj::new(res)?.into())
}
SwipeFlow::new(&ContinueRecoveryBetweenShares::Main)?
.with_page(&ContinueRecoveryBetweenShares::Main, content_main)?
.with_page(&ContinueRecoveryBetweenShares::Menu, content_menu)?
.with_page(
&ContinueRecoveryBetweenShares::CancelIntro,
content_cancel_intro,
)?
.with_page(
&ContinueRecoveryBetweenShares::CancelConfirm,
content_cancel_confirm,
)?
} else {
let content_menu = Frame::left_aligned(
TString::empty(),
VerticalMenu::empty()
.item(
theme::ICON_CHEVRON_RIGHT,
TR::recovery__title_remaining_shares.into(),
)
.danger(theme::ICON_CANCEL, cancel_btn.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let n_remaining_shares = pars_show_shares.len();
let content_remaining_shares = Frame::left_aligned(
TR::recovery__title_remaining_shares.into(),
SwipeContent::new(SwipePage::vertical(pars_show_shares.into_paragraphs())),
)
.with_cancel_button()
// .with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Left, SwipeSettings::default())
.with_vertical_pages()
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Cancelled))
.repeated_button_request(ButtonRequest::new(
ButtonRequestCode::Other,
"show_shares".into(),
))
.with_pages(move |_| n_remaining_shares);
SwipeFlow::new(&ContinueRecoveryBetweenSharesAdvanced::Main)?
.with_page(&ContinueRecoveryBetweenSharesAdvanced::Main, content_main)?
.with_page(&ContinueRecoveryBetweenSharesAdvanced::Menu, content_menu)?
.with_page(
&ContinueRecoveryBetweenSharesAdvanced::CancelIntro,
content_cancel_intro,
)?
.with_page(
&ContinueRecoveryBetweenSharesAdvanced::CancelConfirm,
content_cancel_confirm,
)?
.with_page(
&ContinueRecoveryBetweenSharesAdvanced::RemainingShares,
content_remaining_shares,
)?
};
Ok(LayoutObj::new(res)?.into())
}

View File

@ -1013,31 +1013,6 @@ extern "C" fn new_show_group_share_success(
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let pages_iterable: Obj = kwargs.get(Qstr::MP_QSTR_pages)?;
let mut paragraphs = ParagraphVecLong::new();
for page in IterBuf::new().try_iterate(pages_iterable)? {
let [title, description]: [TString; 2] = util::iter_into_array(page)?;
paragraphs
.add(Paragraph::new(&theme::TEXT_DEMIBOLD, title))
.add(Paragraph::new(&theme::TEXT_NORMAL, description).break_after());
}
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(
TR::recovery__title_remaining_shares.into(),
SwipeContent::new(paragraphs.into_paragraphs()),
)
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default()),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
@ -1594,6 +1569,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// recovery_type: RecoveryType,
/// text: str,
/// subtext: str | None = None,
/// pages: Iterable[tuple[str, str]] | None = None,
/// ) -> LayoutObj[UiResult]:
/// """Device recovery homescreen."""
Qstr::MP_QSTR_flow_continue_recovery => obj_fn_kw!(0, flow::continue_recovery::new_continue_recovery).as_obj(),
@ -1613,13 +1589,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Shown after successfully finishing a group."""
Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(),
/// def show_remaining_shares(
/// *,
/// pages: Iterable[tuple[str, str]],
/// ) -> LayoutObj[UiResult]:
/// """Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(),
/// def show_progress(
/// *,
/// title: str,

View File

@ -452,6 +452,7 @@ def flow_continue_recovery(
recovery_type: RecoveryType,
text: str,
subtext: str | None = None,
pages: Iterable[tuple[str, str]] | None = None,
) -> LayoutObj[UiResult]:
"""Device recovery homescreen."""
@ -473,14 +474,6 @@ def show_group_share_success(
"""Shown after successfully finishing a group."""
# rust/src/ui/model_mercury/layout.rs
def show_remaining_shares(
*,
pages: Iterable[tuple[str, str]],
) -> LayoutObj[UiResult]:
"""Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
# rust/src/ui/model_mercury/layout.rs
def show_progress(
*,

View File

@ -13,6 +13,8 @@ from . import layout, recover
if TYPE_CHECKING:
from trezor.enums import BackupType, RecoveryType
from .layout import RemainingSharesInfo
async def recovery_homescreen() -> None:
from trezor import workflow
@ -297,7 +299,7 @@ async def _request_share_next_screen() -> None:
await layout.homescreen_dialog(
TR.buttons__enter,
TR.recovery__more_shares_needed,
info_func=_show_remaining_groups_and_shares,
remaining_shares_info=_get_remaining_groups_and_shares(),
)
else:
still_needed_shares = remaining[0]
@ -315,21 +317,21 @@ async def _request_share_next_screen() -> None:
await layout.homescreen_dialog(TR.buttons__enter_share, entered, needed)
async def _show_remaining_groups_and_shares() -> None:
def _get_remaining_groups_and_shares() -> "RemainingSharesInfo":
"""
Show info dialog for Slip39 Advanced - what shares are to be entered.
Prepare data for Slip39 Advanced - what shares are to be entered.
"""
from trezor.crypto import slip39
shares_remaining = storage_recovery.fetch_slip39_remaining_shares()
# should be stored at this point
assert shares_remaining
assert shares_remaining # should be stored at this point
groups = set()
first_entered_index = -1
for i, group_count in enumerate(shares_remaining):
if group_count < slip39.MAX_SHARE_COUNT:
first_entered_index = i
break
share = None
for index, remaining in enumerate(shares_remaining):
@ -337,15 +339,15 @@ async def _show_remaining_groups_and_shares() -> None:
m = storage_recovery_shares.fetch_group(index)[0]
if not share:
share = slip39.decode_mnemonic(m)
identifier = m.split(" ")[0:3]
groups.add((remaining, tuple(identifier)))
identifier = tuple(m.split(" ")[0:3])
groups.add(identifier)
elif remaining == slip39.MAX_SHARE_COUNT: # no shares yet
identifier = storage_recovery_shares.fetch_group(first_entered_index)[
0
].split(" ")[0:2]
groups.add((remaining, tuple(identifier)))
identifier = tuple(
storage_recovery_shares.fetch_group(first_entered_index)[0].split(" ")[
0:2
]
)
groups.add(identifier)
assert share # share needs to be set
return await layout.show_remaining_shares(
groups, shares_remaining, share.group_threshold
)
return groups, shares_remaining, share.group_threshold

View File

@ -6,16 +6,19 @@ from trezor.ui.layouts.recovery import ( # noqa: F401
request_word_count,
show_group_share_success,
show_recovery_warning,
show_remaining_shares,
)
from apps.common import backup_types
if TYPE_CHECKING:
from typing import Callable
from trezor.enums import BackupType
# RemainingSharesInfo represents the data structure for remaining shares in SLIP-39 recovery:
# - Set of tuples, each containing 2 or 3 words identifying a group
# - List of remaining share counts for each group
# - Group threshold (minimum number of groups required)
RemainingSharesInfo = tuple[set[tuple[str, ...]], list[int], int]
async def request_mnemonic(
word_count: int, backup_type: BackupType | None
@ -123,8 +126,8 @@ async def homescreen_dialog(
button_label: str,
text: str,
subtext: str | None = None,
info_func: Callable | None = None,
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> None:
import storage.recovery as storage_recovery
from trezor.ui.layouts.recovery import continue_recovery
@ -133,6 +136,11 @@ async def homescreen_dialog(
recovery_type = storage_recovery.get_type()
if not await continue_recovery(
button_label, text, subtext, info_func, recovery_type, show_info
button_label,
text,
subtext,
recovery_type,
show_info,
remaining_shares_info,
):
raise RecoveryAborted

View File

@ -1,4 +1,4 @@
from typing import Callable, Iterable
from typing import TYPE_CHECKING
import trezorui2
from trezor import TR
@ -11,6 +11,9 @@ CONFIRMED = trezorui2.CONFIRMED # global_import_cache
CANCELLED = trezorui2.CANCELLED # global_import_cache
INFO = trezorui2.INFO # global_import_cache
if TYPE_CHECKING:
from apps.management.recovery_device.layout import RemainingSharesInfo
async def request_word_count(recovery_type: RecoveryType) -> int:
selector = RustLayout(trezorui2.select_word_count(recovery_type=recovery_type))
@ -40,16 +43,18 @@ async def request_word(
return word
async def show_remaining_shares(
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
shares_remaining: list[int],
group_threshold: int,
) -> None:
def format_remaining_shares_info(
remaining_shares_info: "RemainingSharesInfo",
) -> list[tuple[str, str]]:
from trezor import strings
from trezor.crypto.slip39 import MAX_SHARE_COUNT
groups, shares_remaining, group_threshold = remaining_shares_info
pages: list[tuple[str, str]] = []
for remaining, group in groups:
completed_groups = shares_remaining.count(0)
for group, remaining in zip(groups, shares_remaining):
if 0 < remaining < MAX_SHARE_COUNT:
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
@ -58,10 +63,8 @@ async def show_remaining_shares(
)
words = "\n".join(group)
pages.append((title, words))
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
groups_remaining = group_threshold - shares_remaining.count(0)
elif remaining == MAX_SHARE_COUNT and completed_groups < group_threshold:
groups_remaining = group_threshold - completed_groups
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
groups_remaining,
@ -70,13 +73,7 @@ async def show_remaining_shares(
words = "\n".join(group)
pages.append((title, words))
await raise_if_not_confirmed(
interact(
RustLayout(trezorui2.show_remaining_shares(pages=pages)),
"show_shares",
ButtonRequestType.Other,
)
)
return pages
async def show_group_share_success(share_index: int, group_index: int) -> None:
@ -102,26 +99,25 @@ async def continue_recovery(
button_label: str, # unused on mercury
text: str,
subtext: str | None,
info_func: Callable | None, # TODO: see below
recovery_type: RecoveryType,
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
# TODO: info_func should be changed to return data to be shown (and not show
# them) so that individual models can implement showing logic on their own.
# T3T1 should move the data to `flow_continue_recovery` and hide them
# in the context menu
# NOTE: show_info can be understood as first screen before any shares
# NOTE: button request sent from the flow
homepage = RustLayout(
result = await RustLayout(
trezorui2.flow_continue_recovery(
first_screen=show_info,
recovery_type=recovery_type,
text=text,
subtext=subtext,
pages=(
format_remaining_shares_info(remaining_shares_info)
if remaining_shares_info
else None
),
)
)
result = await homepage
return result is CONFIRMED

View File

@ -1,4 +1,4 @@
from typing import Callable, Iterable
from typing import TYPE_CHECKING, Iterable
import trezorui2
from trezor import TR
@ -7,6 +7,9 @@ from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact
from . import RustLayout, raise_if_not_confirmed, show_warning
if TYPE_CHECKING:
from apps.management.recovery_device.layout import RemainingSharesInfo
async def request_word_count(recovery_type: RecoveryType) -> int:
count = await interact(
@ -97,9 +100,9 @@ async def continue_recovery(
button_label: str,
text: str,
subtext: str | None,
info_func: Callable | None,
recovery_type: RecoveryType,
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None, # unused on TR
) -> bool:
# TODO: implement info_func?
# There is very limited space on the screen

View File

@ -1,4 +1,4 @@
from typing import Callable, Iterable
from typing import TYPE_CHECKING, Callable
import trezorui2
from trezor import TR
@ -10,6 +10,9 @@ from . import RustLayout, raise_if_not_confirmed
CONFIRMED = trezorui2.CONFIRMED # global_import_cache
INFO = trezorui2.INFO # global_import_cache
if TYPE_CHECKING:
from apps.management.recovery_device.layout import RemainingSharesInfo
async def _homepage_with_info(
dialog: RustLayout,
@ -54,7 +57,7 @@ async def request_word(
async def show_remaining_shares(
groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words
groups: set[tuple[str, ...]],
shares_remaining: list[int],
group_threshold: int,
) -> None:
@ -62,7 +65,9 @@ async def show_remaining_shares(
from trezor.crypto.slip39 import MAX_SHARE_COUNT
pages: list[tuple[str, str]] = []
for remaining, group in groups:
completed_groups = shares_remaining.count(0)
for group, remaining in zip(groups, shares_remaining):
if 0 < remaining < MAX_SHARE_COUNT:
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
@ -71,10 +76,8 @@ async def show_remaining_shares(
)
words = "\n".join(group)
pages.append((title, words))
elif (
remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold
):
groups_remaining = group_threshold - shares_remaining.count(0)
elif remaining == MAX_SHARE_COUNT and completed_groups < group_threshold:
groups_remaining = group_threshold - completed_groups
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
groups_remaining,
@ -139,9 +142,9 @@ async def continue_recovery(
button_label: str,
text: str,
subtext: str | None,
info_func: Callable | None,
recovery_type: RecoveryType,
show_info: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
from trezor.wire import ActionCancelled
@ -160,19 +163,26 @@ async def continue_recovery(
description=description,
button=button_label,
recovery_type=recovery_type,
info_button=info_func is not None,
info_button=remaining_shares_info is not None,
)
)
await button_request("recovery", ButtonRequestType.RecoveryHomepage)
result = (
await homepage
if info_func is None
else await _homepage_with_info(homepage, info_func)
)
if remaining_shares_info is None:
result = await homepage
else:
groups, shares_remaining, group_threshold = remaining_shares_info
result = await _homepage_with_info(
homepage,
lambda: show_remaining_shares(
groups, shares_remaining, group_threshold
),
)
if result is CONFIRMED:
return True
try:
await _confirm_abort(recovery_type != RecoveryType.NormalRecovery)
except ActionCancelled:

View File

@ -73,7 +73,6 @@ def test_secret(client: Client, shares: list[str], secret: str):
_test_secret(client, shares, secret)
@pytest.mark.skip_t3t1(reason="currently broken on T3T1")
@pytest.mark.parametrize("shares, secret", VECTORS)
@pytest.mark.setup_client(uninitialized=True)
def test_secret_click_info_button(client: Client, shares: list[str], secret: str):

View File

@ -304,7 +304,7 @@ class RecoveryFlow:
if self.client.model is models.T2T1:
yield from self.tt_click_info()
elif self.client.model is models.T3T1:
self.mercury_click_info()
yield from self.mercury_click_info()
yield from self.success_more_shares_needed()
def tt_click_info(
@ -316,10 +316,19 @@ class RecoveryFlow:
self.debug.swipe_up()
self.debug.press_yes()
def mercury_click_info(self):
def mercury_click_info(self) -> BRGeneratorType:
# Starting on the homepage, handle the repeated button request
br = yield
assert br.name == "recovery"
assert br.code == B.RecoveryHomepage
# Moving through the menu into the show_shares screen
self.debug.click(buttons.CORNER_BUTTON, wait=True)
self.debug.synchronize_at("VerticalMenu")
self.debug.click(buttons.VERTICAL_MENU[0], wait=True)
br = yield
assert br.name == "show_shares"
assert br.code == B.Other
# Getting back to the homepage
self.debug.click(buttons.CORNER_BUTTON, wait=True)
self.debug.click(buttons.CORNER_BUTTON, wait=True)

View File

@ -16511,6 +16511,8 @@
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "f207194fc64f068391f521c2069ef1fcf55c581c1c241c49c620f9462557fcf1",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "ad415d9e066ae572abc1e81df83758f1eb884aa75d33464ad5cf393edd17c2ac",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "e8a1825029d6cd6fccf2d9433df5153cc7494c00350c0cab09300fc05bff8a87",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "987214495bb12d72ef099fcf6a5e3e671eda37ed5905e552256c300a7934d919",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "2a1e35ba924989ed4d0aa87b9ddfd61ff91084d142bb517a255c517e82f8463f",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "06b9b90d151e8eb32f824b25c379e38dccd2732592a9d54cbe963b75b88cef24",
"T3T1_cs_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "908dfde70e1269935c7add3c2edaea223724fc78bc8e030e5aeb08dd4fd0ec9f",
"T3T1_cs_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "65a86c715390ceca75c9eb2f56a54bf85e2b915489bc080ca45f15a69c5dc55d",
@ -17853,6 +17855,8 @@
"T3T1_de_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "d585bf013c0c6526bd9dc17f33bb21c0de19e2d5e653159d5821921f32da2705",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "03437b32a72c828dbb6365be52090ca3bdabdc969cd8e238b88b8fff868b0417",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "aca43fcd50b60741f3dda0e06adb01d74c41cf92853368759c476e6fe55326a9",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "a8b0d2f295238aa40db569320fd715a017f98244887d3366d0a9b7a4ac5460cd",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "379fc0b4041a4085e2cb41dfd922c1dcddaca4a08da9afe6e72dd0fce01137e4",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "af836476467f13d07ca7689fef5e9703e85f0ec489c0f3528b5fa9ccc9531041",
"T3T1_de_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "f238d4b9f4f6d13fb082a2b048a7aed6a3f25281ce637d973f3a3cdfedbb5909",
"T3T1_de_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "2fc4b3a3ab644a339238a7291a162b1e1fbed9714cae4de49b7e1e10814af2bf",
@ -19195,6 +19199,8 @@
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "fb29b0dcf08f0ac969197d48ab9547850846959d09ac215aa5a831b2432e3d3b",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "e7748f6864c05f4a81677d0c70baf12c16eb3463f1f6f1f6caf99b89752318ca",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "f26e7ad1434f037331503f436e82780c7f3e4c9d6389b064680f1098d82fb2fc",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "6b1dab1a11ae8853e1dbfbdea8362843f9b31b069824f6484d6e8cb00eeaa1f0",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "b9a12d0387b2f74769366176b53fe405e557f4af329fb4dcd3b2eecccf8ad07a",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "ff9fd97ab6ca805596bba50ff532a5decded71da4888456d6bc4f6aa6992cc4a",
"T3T1_en_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "49149d6d43bbf9fd59f92685569e541fc01a8765e60580518a9b75063bad89ba",
"T3T1_en_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "a23cd874215a2944ef6282f9e396715d3f629b9afa3da0dd534049ce6cc323cd",
@ -20537,6 +20543,8 @@
"T3T1_es_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "bfcc90188c0e704f2b2ee4afd7c616e116493bf0f9ef08aeb006c8eae23e3130",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "c86b3f84b601aa162ebfa59e49f7a83ece8b3d03f780ca6cffba01e257f9e631",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "739e3359b4c0d22da0ce39c1949d36e50942fa6855941c19bb5ef8a33b10e63a",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "d8b4392d7b9c05436504d714e5b5be17229afe0020db212d5bad30618570ae52",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "e223d9a2b11fb83b76dc6266c285d9a853db04fbeeecd5d794206042a06159e6",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "95dc3c3561517b60adb1c2021ea23b9ec11e6b63851e11b4586fbf1762bea32b",
"T3T1_es_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "83bbf55f0a13974b9f7832554aa3019ae16d56555f017ecb5130c7594637e775",
"T3T1_es_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "e046d946894f3308666456016642ae88826ddcfb4253e7d90f29e410b3dc14fe",
@ -21879,6 +21887,8 @@
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "c2a537f28eac80fcf674c4f2c1f00c82dd618b06d5b030ad86cf5a7f54178c27",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "fc949c187da623bf33b5bfdb034d9933d3edd0551b4355f70c6347420989939d",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "e8a0a40d53e0f359d1b4178bbf14d7a372f4f56918819fdd9ed5806d515e1b7a",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "804b8a27ecdde765cf636c8358f30cb51d1cbfa9020c2ed1690bb881d9e6d649",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "0eee15f6168a094643bd299c9d7a9d5e5942340ee46a27556b0efb081110ab73",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "3c43ed6e7e58eabe8b7db49124c2ec0202fcfc3f50cb15449e24ffeb716c090d",
"T3T1_fr_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "08306b13088a2fcca5beaeb6b97c4071a149b399fb607b82e1c57352d4ba78e0",
"T3T1_fr_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "7eca9e95d1798c4762fd052d976eb080374b7e8cf96ce1fc01b7f9ad25717bcf",