mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-19 05:58:09 +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:
parent
708b0274f5
commit
c5f9fadbd1
1
core/.changelog.d/4142.fixed
Normal file
1
core/.changelog.d/4142.fixed
Normal file
@ -0,0 +1 @@
|
||||
[T3T1] Added missing info about remaining shares in super-shamir recovery.
|
@ -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,19 +107,62 @@ 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> {
|
||||
#[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) }
|
||||
}
|
||||
|
||||
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_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 (title, cancel_btn, cancel_title, cancel_intro) = match recovery_type {
|
||||
RecoveryType::Normal => (
|
||||
@ -125,27 +179,27 @@ impl ContinueRecoveryBeforeShares {
|
||||
),
|
||||
};
|
||||
|
||||
let mut pars = ParagraphVecShort::new();
|
||||
let mut pars_main = ParagraphVecShort::new();
|
||||
let footer_instruction;
|
||||
let footer_description;
|
||||
if first_screen {
|
||||
pars.add(Paragraph::new(
|
||||
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.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
|
||||
pars_main.add(Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, text));
|
||||
if let Some(sub) = subtext {
|
||||
pars.add(Paragraph::new(&theme::TEXT_SUB_GREY, sub));
|
||||
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 paragraphs_main = Paragraphs::new(pars);
|
||||
let content_main = Frame::left_aligned(title.into(), SwipeContent::new(paragraphs_main))
|
||||
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,18 +209,8 @@ impl ContinueRecoveryBeforeShares {
|
||||
.repeated_button_request(ButtonRequest::new(
|
||||
ButtonRequestCode::RecoveryHomepage,
|
||||
"recovery".into(),
|
||||
));
|
||||
|
||||
let content_menu = Frame::left_aligned(
|
||||
TString::empty(),
|
||||
VerticalMenu::empty().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),
|
||||
});
|
||||
))
|
||||
.with_pages(|_| 1);
|
||||
|
||||
let paragraphs_cancel = ParagraphVecShort::from_iter([
|
||||
Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, cancel_intro).with_bottom_padding(17),
|
||||
@ -206,10 +250,32 @@ impl ContinueRecoveryBeforeShares {
|
||||
});
|
||||
|
||||
let res = if first_screen {
|
||||
let content_menu = Frame::left_aligned(
|
||||
TString::empty(),
|
||||
VerticalMenu::empty().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),
|
||||
});
|
||||
|
||||
SwipeFlow::new(&ContinueRecoveryBeforeShares::Main)?
|
||||
.with_page(&ContinueRecoveryBeforeShares::Main, content_main)?
|
||||
.with_page(&ContinueRecoveryBeforeShares::Menu, content_menu)?
|
||||
} else {
|
||||
} 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_swipe(SwipeDirection::Right, SwipeSettings::immediate())
|
||||
.map(|msg| match msg {
|
||||
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
|
||||
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
|
||||
});
|
||||
|
||||
SwipeFlow::new(&ContinueRecoveryBetweenShares::Main)?
|
||||
.with_page(&ContinueRecoveryBetweenShares::Main, content_main)?
|
||||
.with_page(&ContinueRecoveryBetweenShares::Menu, content_menu)?
|
||||
@ -221,7 +287,55 @@ impl ContinueRecoveryBeforeShares {
|
||||
&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())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user