WIP - multisig receive flow

grdddj/tr_homescreen_game
grdddj 1 year ago
parent a7cefce84c
commit 0acf94099a

@ -61,7 +61,6 @@ static void _librust_qstrs(void) {
MP_QSTR_share_words;
MP_QSTR_show_checklist;
MP_QSTR_show_group_share_success;
MP_QSTR_show_qr;
MP_QSTR_show_remaining_shares;
MP_QSTR_show_share_words;
MP_QSTR_show_progress;

@ -54,6 +54,10 @@ where
}
}
pub fn set_text(&mut self, text: T) {
self.text = text;
}
pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) {
if let State::Initial = self.state {
let text_width = self.font.text_width(self.text.as_ref());

@ -1,11 +1,8 @@
use super::iter::GlyphMetrics;
use crate::{
micropython::buffer::StrBuffer,
ui::{
display,
display::{Color, Font, toif::Icon},
geometry::{Alignment, Dimensions, Offset, Point, Rect, BOTTOM_LEFT},
},
use crate::ui::{
display,
display::{toif::Icon, Color, Font},
geometry::{Alignment, Dimensions, Offset, Point, Rect, BOTTOM_LEFT},
};
const ELLIPSIS: &str = "...";
@ -34,26 +31,6 @@ pub enum PageBreaking {
CutAndInsertEllipsisBoth,
}
/// Holding information about a QR code to be displayed.
#[derive(Clone)]
pub struct QrCodeInfo {
pub data: StrBuffer,
pub max_size: i16,
pub case_sensitive: bool,
pub center: Point,
}
impl QrCodeInfo {
pub fn new(data: StrBuffer, max_size: i16, case_sensitive: bool, center: Point) -> Self {
Self {
data,
max_size,
case_sensitive,
center,
}
}
}
/// Visual instructions for laying out a formatted block of text.
#[derive(Copy, Clone)]
pub struct TextLayout {
@ -206,11 +183,7 @@ impl TextLayout {
/// Draw as much text as possible on the current screen.
pub fn render_text(&self, text: &str) {
self.layout_text(
text,
&mut self.initial_cursor(),
&mut TextRenderer,
);
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
}
/// Y coordinate of the bottom of the available space/bounds
@ -426,8 +399,6 @@ impl LayoutFit {
pub trait LayoutSink {
/// Text should be processed.
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
/// QR code should be displayed.
fn qrcode(&mut self, _qr_code: QrCodeInfo) {}
/// Hyphen at the end of line.
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Ellipsis at the end of the page.
@ -509,22 +480,6 @@ impl LayoutSink for TextRenderer {
);
}
}
fn qrcode(&mut self, qr_code: QrCodeInfo) {
// TODO: replace this by new QR code
// OR
// Get rid of this and use the same flow as TT
// OR
// Use it for TT as well
// display::text_left(qr_code.center, qr_code.data.as_ref(), layout.style.text_font, layout.style.text_color, layout.style.background_color)
// display::qrcode(
// qr_code.center,
// qr_code.data.as_ref(),
// qr_code.max_size as _,
// qr_code.case_sensitive,
// )
// .unwrap_or(())
}
}
#[cfg(feature = "ui_debug")]
@ -541,11 +496,6 @@ pub mod trace {
self.0.string(text);
}
fn qrcode(&mut self, qr_code: QrCodeInfo) {
self.0.string("QR code: ");
self.0.string(qr_code.data.as_ref());
}
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string("-");
}

@ -0,0 +1,270 @@
use heapless::Vec;
use crate::{
error::Error,
micropython::buffer::StrBuffer,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
},
geometry::Rect,
model_tr::theme,
},
};
use super::{ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, Frame};
const MAX_XPUBS: usize = 16;
pub enum AddressDetailsMsg {
Cancelled,
}
pub struct AddressDetails {
qr_code: Qr,
details_view: Paragraphs<ParagraphVecShort<StrBuffer>>,
xpub_view: Frame<Paragraphs<Paragraph<StrBuffer>>>,
xpubs: Vec<(StrBuffer, StrBuffer), MAX_XPUBS>,
current_page: usize,
current_subpage: usize,
area: Rect,
pad: Pad,
buttons: Child<ButtonController>,
}
impl AddressDetails {
pub fn new(
qr_address: StrBuffer,
case_sensitive: bool,
account: Option<StrBuffer>,
path: Option<StrBuffer>,
) -> Result<Self, Error> {
let qr_code = Qr::new(qr_address, case_sensitive)?.with_border(3);
let details_view = {
let mut para = ParagraphVecShort::new();
if let Some(account) = account {
para.add(Paragraph::new(&theme::TEXT_BOLD, "Account:".into()));
para.add(Paragraph::new(&theme::TEXT_MONO, account));
}
if let Some(path) = path {
para.add(Paragraph::new(&theme::TEXT_BOLD, "Derivation path:".into()));
para.add(Paragraph::new(&theme::TEXT_MONO, path));
}
Paragraphs::new(para)
};
let xpub_view = Frame::new(
"".into(),
Paragraph::new(&theme::TEXT_MONO_DATA, "".into()).into_paragraphs(),
);
let result = Self {
qr_code,
details_view,
xpub_view,
xpubs: Vec::new(),
area: Rect::zero(),
current_page: 0,
current_subpage: 0,
pad: Pad::with_background(theme::BG).with_clear(),
buttons: Child::new(ButtonController::new(ButtonLayout::arrow_none_arrow())),
};
Ok(result)
}
pub fn add_xpub(&mut self, title: StrBuffer, xpub: StrBuffer) -> Result<(), Error> {
self.xpubs
.push((title, xpub))
.map_err(|_| Error::OutOfRange)
}
fn is_in_subpage(&self) -> bool {
self.current_subpage > 0
}
fn is_xpub_page(&self) -> bool {
self.current_page > 1
}
fn is_last_page(&self) -> bool {
self.current_page == self.page_count() - 1
}
fn is_last_subpage(&mut self) -> bool {
self.current_subpage == self.subpages_in_current_page() - 1
}
fn subpages_in_current_page(&mut self) -> usize {
if self.is_xpub_page() {
self.xpub_view.inner_mut().page_count()
} else {
1
}
}
// Button layout for the current page.
// Normally there are arrows everywhere, apart from the right side of the last
// page. On xpub pages there is SHOW MORE middle button when it cannot fit
// one page. On xpub subpages there are wide arrows to scroll.
fn get_button_layout(&mut self) -> ButtonLayout {
let (left, middle, right) = if self.is_in_subpage() {
let left = Some(ButtonDetails::up_arrow_icon_wide());
let right = if self.is_last_subpage() {
None
} else {
Some(ButtonDetails::down_arrow_icon_wide())
};
(left, None, right)
} else {
let left = Some(ButtonDetails::left_arrow_icon());
let middle = if self.is_xpub_page() && self.subpages_in_current_page() > 1 {
Some(ButtonDetails::armed_text("SHOW MORE".into()))
} else {
None
};
let right = if self.is_last_page() {
None
} else {
Some(ButtonDetails::right_arrow_icon())
};
(left, middle, right)
};
ButtonLayout::new(left, middle, right)
}
/// Reflecting the current page in the buttons.
fn update_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_button_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
buttons.set(btn_layout);
});
}
fn page_count(&self) -> usize {
2 + self.xpubs.len()
}
fn fill_xpub_page(&mut self, ctx: &mut EventCtx) {
let i = self.current_page - 2;
self.xpub_view.update_title(ctx, self.xpubs[i].0);
self.xpub_view.update_content(ctx, |p| {
p.inner_mut().update(self.xpubs[i].1);
p.change_page(0)
});
}
fn change_page(&mut self, ctx: &mut EventCtx) {
if self.is_xpub_page() {
self.fill_xpub_page(ctx);
}
self.pad.clear();
self.current_subpage = 0;
}
fn change_subpage(&mut self, ctx: &mut EventCtx) {
if self.is_xpub_page() {
self.xpub_view
.update_content(ctx, |p| p.change_page(self.current_subpage));
self.pad.clear();
}
}
}
impl Component for AddressDetails {
type Msg = AddressDetailsMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// QR code is being placed on the whole bounds, so it can be as big as possible
// (it will not collide with the buttons, they are narrow and on the sides).
// Therefore, also placing pad on the whole bounds.
self.qr_code.place(bounds);
self.pad.place(bounds);
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
self.details_view.place(content_area);
self.xpub_view.place(content_area);
self.buttons.place(button_area);
self.area = content_area;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Possibly update the components that have e.g. marquee
match self.current_page {
0 => self.qr_code.event(ctx, event),
1 => self.details_view.event(ctx, event),
_ => self.xpub_view.event(ctx, event),
};
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(button)) = button_event {
if self.is_in_subpage() {
match button {
ButtonPos::Left => {
// Going back
self.current_subpage -= 1;
}
ButtonPos::Right => {
// Going next
self.current_subpage += 1;
}
_ => unreachable!(),
}
self.change_subpage(ctx);
self.update_buttons(ctx);
return None;
} else {
match button {
ButtonPos::Left => {
// Cancelling or going back
if self.current_page == 0 {
return Some(AddressDetailsMsg::Cancelled);
}
self.current_page -= 1;
self.change_page(ctx);
}
ButtonPos::Right => {
// Going to the next page
self.current_page += 1;
self.change_page(ctx);
}
ButtonPos::Middle => {
// Going into subpage
self.current_subpage = 1;
self.change_subpage(ctx);
}
}
self.update_buttons(ctx);
return None;
}
}
None
}
fn paint(&mut self) {
self.pad.paint();
self.buttons.paint();
match self.current_page {
0 => self.qr_code.paint(),
1 => self.details_view.paint(),
_ => self.xpub_view.paint(),
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for AddressDetails {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("AddressDetails");
match self.current_page {
0 => self.qr_code.trace(t),
1 => self.details_view.trace(t),
_ => self.xpub_view.trace(t),
}
t.close();
}
}

@ -691,6 +691,8 @@ pub enum ButtonAction {
Cancel,
/// Confirm the whole layout - send Msg::Confirmed
Confirm,
/// Send INFO message from layout - send Msg::Info
Info,
/// Select current choice value
Select,
/// Some custom specific action
@ -820,6 +822,15 @@ impl ButtonActions {
)
}
/// Cancelling with left, confirming with middle and info with right
pub fn cancel_confirm_info() -> Self {
Self::new(
Some(ButtonAction::Cancel),
Some(ButtonAction::Confirm),
Some(ButtonAction::Info),
)
}
/// Going to the beginning with left, confirming with right
pub fn beginning_none_confirm() -> Self {
Self::new(
@ -900,6 +911,7 @@ impl ButtonAction {
}
ButtonAction::Cancel => "Cancel".into(),
ButtonAction::Confirm => "Confirm".into(),
ButtonAction::Info => "Info".into(),
ButtonAction::Select => "Select".into(),
ButtonAction::Action(action) => (*action).into(),
}

@ -17,6 +17,7 @@ pub enum FlowMsg {
Confirmed,
ConfirmedIndex(usize),
Cancelled,
Info,
}
pub struct Flow<F, const M: usize> {
@ -258,6 +259,7 @@ where
return Some(FlowMsg::Confirmed);
}
}
ButtonAction::Info => return Some(FlowMsg::Info),
ButtonAction::Select => {}
ButtonAction::Action(_) => {}
}

@ -3,13 +3,13 @@ use crate::{
ui::{
component::{
text::{
layout::{LayoutFit, LayoutSink, QrCodeInfo, TextNoOp, TextRenderer},
layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer},
TextStyle,
},
LineBreaking, Paginate, TextLayout,
},
display::{Font, Icon},
geometry::{Alignment, Offset, Point, Rect},
geometry::{Alignment, Offset, Rect},
model_tr::theme,
util::ResultExt,
},
@ -104,8 +104,14 @@ impl<const M: usize> Page<M> {
theme::FG,
theme::FG,
)
.with_ellipsis_icon(Icon::new(theme::ICON_NEXT_PAGE), theme::ELLIPSIS_ICON_MARGIN)
.with_prev_page_icon(Icon::new(theme::ICON_PREV_PAGE), theme::PREV_PAGE_ICON_MARGIN);
.with_ellipsis_icon(
Icon::new(theme::ICON_NEXT_PAGE),
theme::ELLIPSIS_ICON_MARGIN,
)
.with_prev_page_icon(
Icon::new(theme::ICON_PREV_PAGE),
theme::PREV_PAGE_ICON_MARGIN,
);
Self {
ops: Vec::new(),
text_layout: TextLayout::new(style),
@ -223,21 +229,6 @@ impl<const M: usize> Page<M> {
self.with_new_item(Op::NextPage)
}
pub fn qr_code(
self,
text: StrBuffer,
max_size: i16,
case_sensitive: bool,
center: Point,
) -> Self {
self.with_new_item(Op::QrCode(QrCodeInfo::new(
text,
max_size,
case_sensitive,
center,
)))
}
pub fn font(self, font: Font) -> Self {
self.with_new_item(Op::Font(font))
}

@ -5,7 +5,7 @@ use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{
text::layout::{LayoutFit, LayoutSink, QrCodeInfo},
text::layout::{LayoutFit, LayoutSink},
TextLayout,
},
display::{Color, Font},
@ -43,8 +43,6 @@ impl ToDisplay {
pub enum Op {
/// Render text with current color and font.
Text(ToDisplay),
/// Render QR Code.
QrCode(QrCodeInfo),
/// Set current text color.
Color(Color),
/// Set currently used font.
@ -91,10 +89,6 @@ impl TextLayout {
None
}
}
Op::QrCode(_) if skipped < skip_bytes => {
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
}
Op::NextPage if skipped < skip_bytes => {
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
@ -136,21 +130,6 @@ impl TextLayout {
height: self.layout_height(init_cursor, *cursor),
};
}
Op::QrCode(qr_details) => {
self.layout_qr_code(qr_details, sink);
// QR codes are always the last component that can be shown
// on the given page (meaning a series of Op's).
// Throwing Fitting to force the end of the whole page.
// (It would be too complicated to account for it by modifying cursor, etc.,
// and there is not a need for it currently. If we want QR code together
// with some other things on the same screen, just first render the other
// things and do the QR code last.)
total_processed_chars += PROCESSED_CHARS_ONE;
return LayoutFit::Fitting {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
// Drawing text or icon
Op::Text(to_display) => {
// Try to fit text on the current page and if they do not fit,
@ -189,11 +168,4 @@ impl TextLayout {
height: self.layout_height(init_cursor, *cursor),
}
}
/// Fitting the QR code on the current screen.
/// Not returning `LayoutFit`, QR codes are handled differently,
/// they automatically throw out of bounds.
pub fn layout_qr_code(&self, qr_code_info: QrCodeInfo, sink: &mut dyn LayoutSink) {
sink.qrcode(qr_code_info);
}
}

@ -25,15 +25,34 @@ where
}
}
pub fn inner(&self) -> &T {
self.content.inner()
}
/// Aligning the title to the center, instead of the left.
pub fn with_title_centered(mut self) -> Self {
self.title = self.title.with_centered();
self
}
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn inner_mut(&mut self) -> &mut T {
self.content.inner_mut()
}
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: StrBuffer) {
self.title.set_text(ctx, new_title);
}
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where
F: Fn(&mut T) -> R,
{
self.content.mutate(ctx, |ctx, c| {
let res = update_fn(c);
c.request_complete_repaint(ctx);
res
})
}
}
impl<T> Component for Frame<T>

@ -1,3 +1,4 @@
mod address_details;
mod button;
mod button_controller;
mod changing_text;
@ -12,8 +13,8 @@ mod input_methods;
mod loader;
mod no_btn_dialog;
mod page;
mod result;
mod progress;
mod result;
mod result_anim;
mod result_popup;
mod scrollbar;
@ -23,6 +24,7 @@ mod title;
use super::theme;
pub use address_details::{AddressDetails, AddressDetailsMsg};
pub use button::{
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonMsg,
ButtonPos, ButtonStyle, ButtonStyleSheet,
@ -47,8 +49,8 @@ pub use input_methods::{
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg};
pub use page::ButtonPage;
pub use result::ResultScreen;
pub use progress::Progress;
pub use result::ResultScreen;
pub use result_anim::{ResultAnim, ResultAnimMsg};
pub use result_popup::{ResultPopup, ResultPopupMsg};
pub use scrollbar::ScrollBar;

@ -114,7 +114,7 @@ where
ButtonLayout::new(btn_left, None, btn_right)
}
/// Get the let button details, depending whether the page is first, last,
/// Get the left button details, depending whether the page is first, last,
/// or in the middle.
fn get_left_button_details(&self, is_first: bool, is_last: bool) -> Option<ButtonDetails> {
if is_first {

@ -37,6 +37,18 @@ impl Title {
self.title.as_ref()
}
pub fn set_text(&mut self, ctx: &mut EventCtx, new_text: StrBuffer) {
self.title = new_text;
self.marquee.set_text(self.title);
let text_width = theme::FONT_HEADER.text_width(new_text.as_ref());
self.needs_marquee = text_width > self.area.width();
// Resetting the marquee to the beginning and starting it when necessary.
self.marquee.reset();
if self.needs_marquee {
self.marquee.start(ctx, Instant::now());
}
}
/// Display title/header at the top left of the given area.
/// Returning the painted height of the whole header.
pub fn paint_header_left(title: StrBuffer, area: Rect) -> i16 {

@ -43,13 +43,13 @@ use crate::{
use super::{
component::{
ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CancelInfoConfirmMsg, Flow,
FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog,
NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg,
PinEntry, PinEntryMsg, Progress, ShareWords, ShowMore, SimpleChoice, SimpleChoiceMsg,
WordlistEntry, WordlistEntryMsg, WordlistType,
AddressDetails, AddressDetailsMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage,
CancelInfoConfirmMsg, Flow, FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg,
Lockscreen, NoBtnDialog, NoBtnDialogMsg, NumberInput, NumberInputMsg, Page,
PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, ShareWords, ShowMore,
SimpleChoice, SimpleChoiceMsg, WordlistEntry, WordlistEntryMsg, WordlistType,
},
constant, theme,
theme,
};
pub enum CancelConfirmMsg {
@ -117,6 +117,7 @@ where
FlowMsg::Confirmed => Ok(CONFIRMED.as_obj()),
FlowMsg::Cancelled => Ok(CANCELLED.as_obj()),
FlowMsg::ConfirmedIndex(index) => index.try_into(),
FlowMsg::Info => Ok(INFO.as_obj()),
}
}
}
@ -130,6 +131,14 @@ impl ComponentMsgObj for PinEntry {
}
}
impl ComponentMsgObj for AddressDetails {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
AddressDetailsMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl ComponentMsgObj for NumberInput {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -376,6 +385,74 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let account: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?;
let path: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_path)?.try_into_option()?;
// let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?;
// let mut iter_buf = IterBuf::new();
// let iter = Iter::try_from_obj_with_buf(xpubs, &mut iter_buf)?;
// for i in iter {
// let [xtitle, text]: [StrBuffer; 2] = iter_into_array(i)?;
// ad.add_xpub(xtitle, text)?;
// }
// let get_page = move |page_index| {
// // Showing two screens - the recipient address and summary confirmation
// match page_index {
// 0 => {
// // QR CODE
// let btn_layout = ButtonLayout::arrow_none_arrow();
// let btn_actions = ButtonActions::cancel_none_next();
// Page::<15>::new(btn_layout, btn_actions, Font::MONO).qr_code(
// address,
// case_sensitive,
// constant::screen(),
// )
// }
// 1 => {
// // ADDRESS INFO
// let btn_layout = ButtonLayout::arrow_none_none();
// let btn_actions = ButtonActions::prev_none_none();
// Page::<15>::new(btn_layout, btn_actions, Font::MONO)
// .with_line_breaking(LineBreaking::BreakWordsNoHyphen)
// .text_bold("Account:".into())
// .newline()
// .text_mono(unwrap!(account))
// .newline()
// .text_bold("Derivation path:".into())
// .newline()
// .text_mono(unwrap!(path))
// }
// _ => unreachable!(),
// }
// };
// let pages = FlowPages::new(get_page, 2);
// let obj = LayoutObj::new(Flow::new(pages))?;
// Ok(obj.into())
let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?;
let mut iter_buf = IterBuf::new();
let iter = Iter::try_from_obj_with_buf(xpubs, &mut iter_buf)?;
let mut ad = AddressDetails::new(address, case_sensitive, account, path)?;
for i in iter {
let [xtitle, text]: [StrBuffer; 2] = iter_into_array(i)?;
ad.add_xpub(xtitle, text)?;
}
let obj = LayoutObj::new(ad)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -541,70 +618,23 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_receive_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let address_qr: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_qr)?.try_into()?;
let account: StrBuffer = kwargs.get(Qstr::MP_QSTR_account)?.try_into()?;
let derivation_path: StrBuffer = kwargs.get(Qstr::MP_QSTR_derivation_path)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?;
let get_page = move |page_index| {
// Showing two screens - the recipient address and summary confirmation
match page_index {
0 => {
// RECEIVE ADDRESS
let btn_layout = ButtonLayout::cancel_armed_text("CONFIRM".into(), "i".into());
let btn_actions = ButtonActions::last_confirm_next();
Page::<15>::new(btn_layout, btn_actions, Font::BOLD)
.with_line_breaking(LineBreaking::BreakWordsNoHyphen)
.text_bold(title)
.newline()
.newline_half()
.text_mono(address)
}
1 => {
// QR CODE
let btn_layout = ButtonLayout::arrow_none_arrow();
let btn_actions = ButtonActions::prev_none_next();
Page::<15>::new(btn_layout, btn_actions, Font::MONO).qr_code(
address_qr,
theme::QR_SIDE_MAX,
case_sensitive,
constant::screen().center(),
)
}
2 => {
// ADDRESS INFO
let btn_layout = ButtonLayout::arrow_none_none();
let btn_actions = ButtonActions::prev_none_none();
Page::<15>::new(btn_layout, btn_actions, Font::MONO)
.with_line_breaking(LineBreaking::BreakWordsNoHyphen)
.text_bold("Account:".into())
.newline()
.text_mono(account)
.newline()
.text_bold("Derivation path:".into())
.newline()
.text_mono(derivation_path)
}
3 => {
// ADDRESS MISMATCH
let btn_layout = ButtonLayout::arrow_none_text("QUIT".into());
let btn_actions = ButtonActions::beginning_none_cancel();
Page::<15>::new(btn_layout, btn_actions, Font::MONO)
.text_bold("ADDRESS MISMATCH?".into())
.newline()
.newline_half()
.text_mono("Please contact Trezor support at trezor.io/support".into())
}
_ => unreachable!(),
}
assert!(page_index == 0);
let btn_layout = ButtonLayout::cancel_armed_text("CONFIRM".into(), "i".into());
let btn_actions = ButtonActions::cancel_confirm_info();
Page::<15>::new(btn_layout, btn_actions, Font::BOLD)
.with_line_breaking(LineBreaking::BreakWordsNoHyphen)
.text_mono(address)
};
let pages = FlowPages::new(get_page, 4);
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages))?;
let obj = LayoutObj::new(Flow::new(pages).with_common_title(title))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -856,6 +886,27 @@ extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_mismatch() -> Obj {
let block = move || {
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::arrow_none_text("QUIT".into());
let btn_actions = ButtonActions::cancel_none_confirm();
Page::<15>::new(btn_layout, btn_actions, Font::MONO)
.text_bold("ADDRESS MISMATCH?".into())
.newline()
.newline_half()
.text_mono("Please contact Trezor support at trezor.io/support".into())
};
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1246,6 +1297,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),
/// def confirm_address(
/// *,
/// title: str,
/// data: str | bytes,
/// description: str | None, # unused on TR
/// extra: str | None, # unused on TR
/// ) -> object:
/// """Confirm address."""
Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(),
/// def confirm_properties(
/// *,
/// title: str,
@ -1265,6 +1327,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm TOS before device setup."""
Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(),
/// def show_address_details(
/// *,
/// address: str,
/// case_sensitive: bool,
/// account: str | None,
/// path: str | None,
/// xpubs: list[tuple[str, str]],
/// ) -> object:
/// """Show address details - QR code, account, path, cosigner xpubs."""
Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(),
/// def confirm_value(
/// *,
/// title: str,
@ -1315,18 +1388,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm summary of a transaction."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
/// def show_receive_address(
/// *,
/// title: str,
/// address: str,
/// address_qr: str,
/// account: str,
/// derivation_path: str,
/// case_sensitive: bool,
/// ) -> object:
/// """Show receive address together with QR code and details about it."""
Qstr::MP_QSTR_show_receive_address => obj_fn_kw!(0, new_show_receive_address).as_obj(),
/// def tutorial() -> object:
/// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
@ -1363,6 +1424,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Info modal."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_mismatch() -> object:
/// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_0!(new_show_mismatch).as_obj(),
/// def confirm_with_info(
/// *,
/// title: str,

@ -1,6 +1,6 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking},
display::{Color, Font, toif::Icon},
display::{toif::Icon, Color, Font},
geometry::Offset,
};
@ -80,12 +80,6 @@ pub const BUTTON_CONTENT_HEIGHT: i16 = 7;
pub const BUTTON_OUTLINE: i16 = 3;
pub const BUTTON_HEIGHT: i16 = BUTTON_CONTENT_HEIGHT + 2 * BUTTON_OUTLINE;
/// Full-size QR code.
/// Accounting for little larger QR code than the screen,
/// to fit taproot addresses (top and bottom row will not be visible).
// TODO: test if this is visible, as it had problems on T1
pub const QR_SIDE_MAX: i16 = 66;
// How many pixels should be between text and icons.
pub const ELLIPSIS_ICON_MARGIN: i16 = 4;
pub const PREV_PAGE_ICON_MARGIN: i16 = 6;

@ -603,29 +603,6 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let verb_cancel: StrBuffer = kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let buttons = Button::cancel_confirm(
Button::with_text(verb_cancel),
Button::with_text("CONFIRM".into()).styled(theme::button_confirm()),
false,
);
let obj = LayoutObj::new(Frame::left_aligned(
theme::label_title(),
title,
Dialog::new(Qr::new(address, case_sensitive)?.with_border(4), buttons),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
@ -1546,16 +1523,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm TOS before device setup."""
Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(),
/// def show_qr(
/// *,
/// title: str,
/// address: str,
/// verb_cancel: str,
/// case_sensitive: bool,
/// ) -> object:
/// """Show QR code."""
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).as_obj(),
/// def show_address_details(
/// *,
/// address: str,

@ -37,6 +37,17 @@ def confirm_blob(
"""Confirm byte sequence data."""
# rust/src/ui/model_tr/layout.rs
def confirm_address(
*,
title: str,
data: str | bytes,
description: str | None, # unused on TR
extra: str | None, # unused on TR
) -> object:
"""Confirm address."""
# rust/src/ui/model_tr/layout.rs
def confirm_properties(
*,
@ -114,16 +125,15 @@ def confirm_total(
# rust/src/ui/model_tr/layout.rs
def show_receive_address(
def show_address_details(
*,
title: str,
address: str,
address_qr: str,
account: str,
derivation_path: str,
case_sensitive: bool,
account: str | None,
path: str | None,
xpubs: list[tuple[str, str]],
) -> object:
"""Show receive address together with QR code and details about it."""
"""Show address details - QR code, account, path, cosigner xpubs."""
# rust/src/ui/model_tr/layout.rs
@ -165,6 +175,11 @@ def show_info(
"""Info modal."""
# rust/src/ui/model_tr/layout.rs
def show_mismatch() -> object:
"""Warning modal, receiving address mismatch."""
# rust/src/ui/model_tr/layout.rs
def confirm_with_info(
*,
@ -432,17 +447,6 @@ def confirm_reset_device(
"""Confirm TOS before device setup."""
# rust/src/ui/model_tt/layout.rs
def show_qr(
*,
title: str,
address: str,
verb_cancel: str,
case_sensitive: bool,
) -> object:
"""Show QR code."""
# rust/src/ui/model_tt/layout.rs
def show_address_details(
*,

@ -475,33 +475,69 @@ async def show_address(
multisig_index: int | None = None,
xpubs: Sequence[str] = (),
) -> None:
# Will be a marquee in case of multisig
title = (
"RECEIVE ADDRESS\n(MULTISIG)"
"RECEIVE ADDRESS (MULTISIG)"
if multisig_index is not None
else "RECEIVE ADDRESS"
)
await raise_if_not_confirmed(
interact(
while True:
result = await interact(
ctx,
RustLayout(
# TODO: unite with TT::trezorui2.confirm_address
trezorui2.show_receive_address(
title=title.upper(),
address=address,
address_qr=address if address_qr is None else address_qr,
account=account or "Unknown",
derivation_path=path or "Unknown",
case_sensitive=case_sensitive,
trezorui2.confirm_address(
title=title,
data=address,
description="", # unused on TR
extra=None, # unused on TR
)
),
"show_address",
ButtonRequestType.Address,
)
)
# TODO: support showing multisig xpubs?
# TODO: send button requests in the flow above?
# User confirmed with middle button.
if result is CONFIRMED:
break
# User pressed right button, go to address details.
elif result is INFO:
def xpub_title(i: int) -> str:
# Will be marquee (cannot fit one line)
result = f"MULTISIG XPUB #{i + 1}"
result += " (YOURS)" if i == multisig_index else " (COSIGNER)"
return result
result = await interact(
ctx,
RustLayout(
trezorui2.show_address_details(
address=address if address_qr is None else address_qr,
case_sensitive=case_sensitive,
account=account,
path=path,
xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)],
)
),
"show_address_details",
ButtonRequestType.Address,
)
# Can only go back from the address details.
assert result is CANCELLED
# User pressed left cancel button, show mismatch dialogue.
else:
result = await interact(
ctx,
RustLayout(trezorui2.show_mismatch()),
"warning_address_mismatch",
ButtonRequestType.Warning,
)
assert result in (CONFIRMED, CANCELLED)
# Right button aborts action, left goes back to showing address.
if result is CONFIRMED:
raise ActionCancelled
def show_pubkey(

@ -467,7 +467,7 @@ class DebugLink:
now = time.monotonic()
while True:
layout = self.read_layout()
if layout_text in layout.text:
if layout_text in layout.str_content:
return layout
if time.monotonic() - now > timeout:
raise RuntimeError("Timeout waiting for layout")

@ -102,9 +102,6 @@ def test_show_tt(
def test_show_cancel(
client: Client, path: str, script_type: messages.InputScriptType, address: str
):
if client.model == "R":
# TODO: make this work
pytest.skip("NOT YET DONE FOR TR")
with client, pytest.raises(Cancelled):
IF = InputFlowShowAddressQRCodeCancel(client)
client.set_input_flow(IF.get())
@ -240,9 +237,6 @@ def test_show_multisig_xpubs(
xpubs: list[str],
ignore_xpub_magic: bool,
):
if client.features.model == "R":
pytest.skip("XPUBs not ready for model R")
nodes = [
btc.get_public_node(
client,

@ -45,6 +45,7 @@ def test_slip25_disallowed(client: Client):
@pytest.mark.skip_t2
@pytest.mark.skip_tr
def test_legacy_restrictions(client: Client):
path = parse_path("m/46'")
with pytest.raises(TrezorFailure, match="Invalid path for EthereumGetPublicKey"):

@ -439,11 +439,7 @@ def test_hide_passphrase_from_host(client: Client):
yield
layout = client.debug.wait_layout()
assert "confirm passphrase" in layout.get_title().lower()
if client.debug.model == "R":
client.debug.press_right()
layout = client.debug.wait_layout()
assert "confirm passphrase" in layout.title().lower()
assert passphrase in layout.text_content()
client.debug.press_yes()

@ -255,8 +255,16 @@ class InputFlowShowAddressQRCode(InputFlowBase):
def input_flow_tr(self) -> GeneratorType:
yield
# Go into details
self.debug.press_right()
self.debug.press_yes()
yield
# Go through details and back
self.debug.press_right()
self.debug.press_left()
self.debug.press_left()
yield
# Confirm
self.debug.press_middle()
class InputFlowShowAddressQRCodeCancel(InputFlowBase):
@ -279,8 +287,19 @@ class InputFlowShowAddressQRCodeCancel(InputFlowBase):
def input_flow_tr(self) -> GeneratorType:
yield
# Go into details
self.debug.press_right()
yield
# Go through details and back
self.debug.press_right()
self.debug.press_left()
self.debug.press_left()
yield
# Cancel
self.debug.press_left()
yield
# Confirm address mismatch
self.debug.press_right()
self.debug.press_yes()
class InputFlowShowMultisigXPUBs(InputFlowBase):
@ -322,6 +341,44 @@ class InputFlowShowMultisigXPUBs(InputFlowBase):
yield # show address
self.debug.press_yes()
def input_flow_tr(self) -> GeneratorType:
yield # show address
layout = self.debug.wait_layout()
assert "RECEIVE ADDRESS (MULTISIG)" in layout.title()
assert layout.text_content().replace(" ", "") == self.address
self.debug.press_right()
yield # show QR code
assert "Qr" in self.debug.wait_layout().str_content
layout = self.debug.press_right(wait=True)
# address details
assert "Multisig 2 of 3" in layout.str_content
# Three xpub pages with the same testing logic
for xpub_num in range(3):
expected_title = f"MULTISIG XPUB #{xpub_num + 1} " + (
"(YOURS)" if self.index == xpub_num else "(COSIGNER)"
)
layout = self.debug.press_right(wait=True)
assert expected_title in layout.title()
xpub_part_1 = layout.text_content().replace(" ", "")
# Press "SHOW MORE"
layout = self.debug.press_middle(wait=True)
xpub_part_2 = layout.text_content().replace(" ", "")
# Go back
self.debug.press_left(wait=True)
assert self.xpubs[xpub_num] == xpub_part_1 + xpub_part_2
for _ in range(5):
self.debug.press_left()
yield # show address
self.debug.press_left()
yield # address mismatch
self.debug.press_left()
yield # show address
self.debug.press_middle()
class InputFlowPaymentRequestDetails(InputFlowBase):
def __init__(self, client: Client, outputs: list[messages.TxOutputType]):

Loading…
Cancel
Save