1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-27 08:38:07 +00:00

feat(core/ui): redesigned receive flow

[no changelog]
This commit is contained in:
Martin Milata 2023-02-14 23:47:20 +01:00
parent 1b94a7cb7b
commit 4af5939a0b
38 changed files with 1452 additions and 576 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
core/assets/info-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
core/assets/octa-bang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

View File

@ -21,6 +21,7 @@ static void _librust_qstrs(void) {
MP_QSTR_jpeg_test;
MP_QSTR_confirm_action;
MP_QSTR_confirm_homescreen;
MP_QSTR_confirm_address;
MP_QSTR_confirm_blob;
MP_QSTR_confirm_properties;
MP_QSTR_confirm_coinjoin;
@ -40,6 +41,7 @@ static void _librust_qstrs(void) {
MP_QSTR_show_success;
MP_QSTR_show_warning;
MP_QSTR_show_info;
MP_QSTR_show_mismatch;
MP_QSTR_show_simple;
MP_QSTR_request_number;
MP_QSTR_request_pin;
@ -56,6 +58,7 @@ static void _librust_qstrs(void) {
MP_QSTR_show_remaining_shares;
MP_QSTR_show_share_words;
MP_QSTR_show_progress;
MP_QSTR_show_address_details;
MP_QSTR_attach_timer_fn;
MP_QSTR_touch_event;
@ -107,6 +110,7 @@ static void _librust_qstrs(void) {
MP_QSTR_time_ms;
MP_QSTR_app_name;
MP_QSTR_icon_name;
MP_QSTR_account;
MP_QSTR_accounts;
MP_QSTR_indeterminate;
MP_QSTR_notification;
@ -114,4 +118,5 @@ static void _librust_qstrs(void) {
MP_QSTR_bootscreen;
MP_QSTR_skip_first_paint;
MP_QSTR_wrong_pin;
MP_QSTR_xpubs;
}

View File

@ -87,6 +87,12 @@ impl BlendedImage {
}
}
// NOTE: currently this function is used too rarely to justify writing special
// case for unblended image.
pub fn single(icon: Icon, color: Color, area_color: Color) -> Self {
Self::new(icon, icon, color, color, area_color)
}
fn paint_image(&self) {
display::icon_over_icon(
None,

View File

@ -24,9 +24,9 @@ pub use map::Map;
pub use marquee::Marquee;
pub use maybe::Maybe;
pub use pad::Pad;
pub use paginated::{PageMsg, Paginate};
pub use paginated::{AuxPageMsg, PageMsg, Paginate};
pub use painter::Painter;
pub use placed::{FixedHeightBar, GridPlaced};
pub use placed::{FixedHeightBar, Floating, GridPlaced, VSplit};
pub use qr_code::Qr;
pub use text::{
formatted::FormattedText,

View File

@ -3,6 +3,15 @@ use crate::ui::component::{
FormattedText,
};
pub enum AuxPageMsg {
/// Page component was instantiated with BACK button on every page and it
/// was pressed.
GoBack,
/// Page component was configured to react to swipes and user swiped left.
SwipeLeft,
}
/// Common message type for pagination components.
pub enum PageMsg<T, U> {
/// Pass-through from paged component.
@ -12,9 +21,8 @@ pub enum PageMsg<T, U> {
/// "OK" and "Cancel" buttons.
Controls(U),
/// Page component was instantiated with BACK button on every page and it
/// was pressed.
GoBack,
/// Auxilliary events used by exotic pages on touchscreens.
Aux(AuxPageMsg),
}
pub trait Paginate {

View File

@ -1,6 +1,6 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Grid, GridCellSpan, Rect},
geometry::{Alignment, Alignment2D, Grid, GridCellSpan, Insets, Offset, Rect, TOP_RIGHT},
};
pub struct GridPlaced<T> {
@ -121,3 +121,135 @@ where
d.close();
}
}
pub struct Floating<T> {
inner: T,
size: Offset,
border: Offset,
align: Alignment2D,
}
impl<T> Floating<T> {
pub const fn new(size: Offset, border: Offset, align: Alignment2D, inner: T) -> Self {
Self {
inner,
size,
border,
align,
}
}
pub const fn top_right(side: i16, border: i16, inner: T) -> Self {
let size = Offset::uniform(side);
let border = Offset::uniform(border);
Self::new(size, border, TOP_RIGHT, inner)
}
}
impl<T> Component for Floating<T>
where
T: Component,
{
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
let mut border = self.border;
let area = match self.align.0 {
Alignment::Start => bounds.split_left(self.size.x).0,
Alignment::Center => panic!("alignment not supported"),
Alignment::End => {
border.x = -border.x;
bounds.split_right(self.size.x).1
}
};
let area = match self.align.1 {
Alignment::Start => area.split_top(self.size.y).0,
Alignment::Center => panic!("alignment not supported"),
Alignment::End => {
border.y = -border.y;
area.split_bottom(self.size.y).1
}
};
self.inner.place(area.translate(border))
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.inner.event(ctx, event)
}
fn paint(&mut self) {
self.inner.paint()
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Floating<T>
where
T: Component,
T: crate::trace::Trace,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Floating");
d.field("inner", &self.inner);
d.close();
}
}
pub struct VSplit<T, U> {
first: T,
second: U,
width: i16,
spacing: i16,
}
impl<T, U> VSplit<T, U> {
pub const fn new(width: i16, spacing: i16, first: T, second: U) -> Self {
Self {
first,
second,
width,
spacing,
}
}
}
impl<M, T, U> Component for VSplit<T, U>
where
T: Component<Msg = M>,
U: Component<Msg = M>,
{
type Msg = M;
fn place(&mut self, bounds: Rect) -> Rect {
let (left, right) = bounds.split_left(self.width);
let right = right.inset(Insets::left(self.spacing));
self.first.place(left);
self.second.place(right);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.first
.event(ctx, event)
.or_else(|| self.second.event(ctx, event))
}
fn paint(&mut self) {
self.first.paint();
self.second.paint();
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for VSplit<T, U>
where
T: Component + crate::trace::Trace,
U: Component + crate::trace::Trace,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("VSplit");
d.field("first", &self.first);
d.field("second", &self.second);
d.close();
}
}

View File

@ -32,7 +32,7 @@ where
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()),
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
PageMsg::GoBack => unreachable!(),
PageMsg::Aux(_) => Err(Error::TypeError),
}
}
}

View File

@ -0,0 +1,158 @@
use heapless::Vec;
use crate::{
error::Error,
ui::{
component::{
text::paragraphs::{
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
},
Component, Event, EventCtx, Never, Paginate, Qr,
},
geometry::Rect,
},
};
use super::{theme, Frame};
pub struct AddressDetails<T> {
qr_code: Frame<Qr, T>,
details: Frame<Paragraphs<ParagraphVecShort<T>>, T>,
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
xpubs: Vec<(T, T), 16>,
current_page: usize,
}
impl<T> AddressDetails<T>
where
T: ParagraphStrType + From<&'static str>,
{
pub fn new(
qr_address: T,
case_sensitive: bool,
account: Option<T>,
path: Option<T>,
) -> Result<Self, Error> {
let mut para = ParagraphVecShort::new();
if let Some(a) = account {
para.add(Paragraph::new(&theme::TEXT_NORMAL, "Account:".into()));
para.add(Paragraph::new(&theme::TEXT_MONO, a));
}
if let Some(p) = path {
para.add(Paragraph::new(
&theme::TEXT_NORMAL,
"Derivation path:".into(),
));
para.add(Paragraph::new(&theme::TEXT_MONO, p));
}
let result = Self {
qr_code: Frame::left_aligned(
theme::label_title(),
"RECEIVE ADDRESS".into(),
Qr::new(qr_address, case_sensitive)?.with_border(7),
)
.with_border(theme::borders_horizontal_scroll()),
details: Frame::left_aligned(
theme::label_title(),
"RECEIVING TO".into(),
para.into_paragraphs(),
)
.with_border(theme::borders_horizontal_scroll()),
xpub_view: Frame::left_aligned(
theme::label_title(),
" \n ".into(),
Paragraph::new(&theme::TEXT_XPUB, "".into()).into_paragraphs(),
)
.with_border(theme::borders_horizontal_scroll()),
xpubs: Vec::new(),
current_page: 0,
};
Ok(result)
}
pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> {
self.xpubs
.push((title, xpub))
.map_err(|_| Error::OutOfRange)
}
}
impl<T> Paginate for AddressDetails<T>
where
T: ParagraphStrType + Clone,
{
fn page_count(&mut self) -> usize {
2 + self.xpubs.len()
}
fn change_page(&mut self, to_page: usize) {
self.current_page = to_page;
if to_page > 1 {
let i = to_page - 2;
// Context is needed for updating child so that it can request repaint. In this
// case the parent component that handles paging always requests complete
// repaint after page change so we can use a dummy context here.
let mut dummy_ctx = EventCtx::new();
self.xpub_view
.update_title(&mut dummy_ctx, self.xpubs[i].0.clone());
self.xpub_view.update_content(&mut dummy_ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
p.change_page(0)
});
}
}
}
impl<T> Component for AddressDetails<T>
where
T: ParagraphStrType,
{
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.qr_code.place(bounds);
self.details.place(bounds);
self.xpub_view.place(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match self.current_page {
0 => self.qr_code.event(ctx, event),
1 => self.details.event(ctx, event),
_ => self.xpub_view.event(ctx, event),
}
}
fn paint(&mut self) {
match self.current_page {
0 => self.qr_code.paint(),
1 => self.details.paint(),
_ => self.xpub_view.paint(),
}
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
match self.current_page {
0 => self.qr_code.bounds(sink),
1 => self.details.bounds(sink),
_ => self.xpub_view.bounds(sink),
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for AddressDetails<T>
where
T: ParagraphStrType + crate::trace::Trace,
{
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.trace(t),
_ => self.xpub_view.trace(t),
}
t.close();
}
}

View File

@ -2,7 +2,8 @@ use crate::{
time::Duration,
ui::{
component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, GridPlaced, Map, TimerToken,
Component, ComponentExt, Event, EventCtx, FixedHeightBar, Floating, GridPlaced, Map,
Paginate, TimerToken, VSplit,
},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
@ -34,7 +35,7 @@ impl<T> Button<T> {
/// (positive).
pub const BASELINE_OFFSET: i16 = -3;
pub fn new(content: ButtonContent<T>) -> Self {
pub const fn new(content: ButtonContent<T>) -> Self {
Self {
content,
area: Rect::zero(),
@ -46,19 +47,19 @@ impl<T> Button<T> {
}
}
pub fn with_text(text: T) -> Self {
pub const fn with_text(text: T) -> Self {
Self::new(ButtonContent::Text(text))
}
pub fn with_icon(icon: Icon) -> Self {
pub const fn with_icon(icon: Icon) -> Self {
Self::new(ButtonContent::Icon(icon))
}
pub fn with_icon_and_text(content: IconText) -> Self {
pub const fn with_icon_and_text(content: IconText) -> Self {
Self::new(ButtonContent::IconAndText(content))
}
pub fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
pub const fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
Self::new(ButtonContent::IconBlend(bg, fg, fg_offset))
}
@ -425,6 +426,29 @@ impl<T> Button<T> {
Self::cancel_confirm(left, right, right_size_factor)
}
pub fn cancel_confirm_square(
left: Button<T>,
right: Button<T>,
) -> CancelConfirmSquare<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
T: AsRef<str>,
{
theme::button_bar(VSplit::new(
theme::BUTTON_HEIGHT,
theme::BUTTON_SPACING,
left.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
}),
right.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
))
}
pub fn cancel_info_confirm(
confirm: T,
info: T,
@ -542,6 +566,9 @@ type CancelInfoConfirm<T, F0, F1, F2> = FixedHeightBar<(
Map<GridPlaced<Button<T>>, F2>,
)>;
type CancelConfirmSquare<T, F0, F1> =
FixedHeightBar<VSplit<Map<Button<T>, F0>, Map<Button<T>, F1>>>;
pub enum CancelInfoConfirmMsg {
Cancelled,
Info,
@ -610,3 +637,87 @@ impl IconText {
}
}
}
pub struct FloatingButton<T> {
inner: T,
button: Floating<Button<&'static str>>,
}
pub enum FloatingButtonMsg<T> {
ButtonClicked,
Content(T),
}
impl<T> FloatingButton<T>
where
T: Component,
{
pub const fn top_right_corner(icon: Icon, inner: T) -> Self {
Self {
inner,
button: Floating::top_right(
theme::CORNER_BUTTON_SIDE,
theme::CORNER_BUTTON_SPACING,
Button::with_icon(icon),
),
}
}
pub fn inner(&self) -> &T {
&self.inner
}
}
impl<T> Component for FloatingButton<T>
where
T: Component,
{
type Msg = FloatingButtonMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
self.button.place(bounds);
self.inner.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
return Some(FloatingButtonMsg::ButtonClicked);
}
self.inner.event(ctx, event).map(FloatingButtonMsg::Content)
}
fn paint(&mut self) {
self.inner.paint();
self.button.paint();
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.inner.bounds(sink);
self.button.bounds(sink);
}
}
impl<T> Paginate for FloatingButton<T>
where
T: Paginate,
{
fn page_count(&mut self) -> usize {
self.inner.page_count()
}
fn change_page(&mut self, to_page: usize) {
self.inner.change_page(to_page)
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for FloatingButton<T>
where
T: Component + crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("FloatingButton");
t.field("inner", self.inner());
t.close();
}
}

View File

@ -1,8 +1,11 @@
use crate::ui::{
component::{
image::BlendedImage,
text::paragraphs::{
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
text::{
paragraphs::{
Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt,
},
TextStyle,
},
Child, Component, Event, EventCtx, Never,
},
@ -116,15 +119,19 @@ where
}
}
pub fn with_description(mut self, description: T) -> Self {
if !description.as_ref().is_empty() {
pub fn with_text(mut self, style: &'static TextStyle, text: T) -> Self {
if !text.as_ref().is_empty() {
self.paragraphs
.inner_mut()
.add(Paragraph::new(&theme::TEXT_NORMAL_OFF_WHITE, description).centered());
.add(Paragraph::new(style, text).centered());
}
self
}
pub fn with_description(self, description: T) -> Self {
self.with_text(&theme::TEXT_NORMAL_OFF_WHITE, description)
}
pub fn new_shares(lines: [T; 4], controls: U) -> Self {
let [l0, l1, l2, l3] = lines;
Self {

View File

@ -1,6 +1,8 @@
use super::theme;
use crate::ui::{
component::{label::Label, text::TextStyle, Child, Component, Event, EventCtx},
component::{
base::ComponentExt, label::Label, text::TextStyle, Child, Component, Event, EventCtx,
},
display::{self, toif::Icon, Color},
geometry::{Alignment, Insets, Offset, Rect},
util::icon_text_center,
@ -45,6 +47,23 @@ where
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: U) {
self.title.mutate(ctx, |ctx, t| {
t.set_text(new_title);
t.request_complete_repaint(ctx)
})
}
pub fn update_content<F>(&mut self, ctx: &mut EventCtx, update_fn: F)
where
F: Fn(&mut T),
{
self.content.mutate(ctx, |ctx, c| {
update_fn(c);
c.request_complete_repaint(ctx)
})
}
}
impl<T, U> Component for Frame<T, U>

View File

@ -0,0 +1,144 @@
use crate::ui::{
component::{
base::ComponentExt, AuxPageMsg, Component, Event, EventCtx, Never, Pad, PageMsg, Paginate,
},
display::{self, Color},
geometry::Rect,
};
use super::{theme, ScrollBar, Swipe, SwipeDirection};
const SCROLLBAR_HEIGHT: i16 = 32;
pub struct HorizontalPage<T> {
content: T,
pad: Pad,
swipe: Swipe,
scrollbar: ScrollBar,
swipe_right_to_go_back: bool,
fade: Option<u16>,
}
impl<T> HorizontalPage<T>
where
T: Paginate,
T: Component,
{
pub fn new(content: T, background: Color) -> Self {
Self {
content,
swipe: Swipe::new(),
pad: Pad::with_background(background),
scrollbar: ScrollBar::horizontal(),
swipe_right_to_go_back: false,
fade: None,
}
}
pub fn with_swipe_right_to_go_back(mut self) -> Self {
self.swipe_right_to_go_back = true;
self
}
pub fn inner(&self) -> &T {
&self.content
}
fn setup_swipe(&mut self) {
self.swipe.allow_left = self.scrollbar.has_next_page();
self.swipe.allow_right = self.scrollbar.has_previous_page() || self.swipe_right_to_go_back;
}
fn on_page_change(&mut self, ctx: &mut EventCtx) {
// Adjust the swipe parameters according to the scrollbar.
self.setup_swipe();
// Change the page in the content, make sure it gets completely repainted and
// clear the background under it.
self.content.change_page(self.scrollbar.active_page);
self.content.request_complete_repaint(ctx);
self.pad.clear();
// Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint.
self.fade = Some(theme::BACKLIGHT_NORMAL);
}
}
impl<T> Component for HorizontalPage<T>
where
T: Paginate,
T: Component,
{
type Msg = PageMsg<T::Msg, Never>;
fn place(&mut self, bounds: Rect) -> Rect {
self.swipe.place(bounds);
let (content, scrollbar) = bounds.split_bottom(SCROLLBAR_HEIGHT);
self.pad.place(content);
self.content.place(content);
self.scrollbar.place(scrollbar);
self.scrollbar
.set_count_and_active_page(self.content.page_count(), 0);
self.setup_swipe();
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.scrollbar.page_count);
if let Some(swipe) = self.swipe.event(ctx, event) {
match swipe {
SwipeDirection::Left => {
self.scrollbar.go_to_next_page();
self.on_page_change(ctx);
return None;
}
SwipeDirection::Right => {
if self.swipe_right_to_go_back && self.scrollbar.active_page == 0 {
return Some(PageMsg::Aux(AuxPageMsg::GoBack));
}
self.scrollbar.go_to_previous_page();
self.on_page_change(ctx);
return None;
}
_ => {
// Ignore other directions.
}
}
}
self.content.event(ctx, event).map(PageMsg::Content)
}
fn paint(&mut self) {
self.pad.paint();
self.content.paint();
self.scrollbar.paint();
if let Some(val) = self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(val);
}
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
self.scrollbar.bounds(sink);
self.content.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for HorizontalPage<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("HorizontalPage");
t.field("active_page", &self.scrollbar.active_page);
t.field("page_count", &self.scrollbar.page_count);
t.field("content", &self.content);
t.close();
}
}

View File

@ -1,3 +1,4 @@
mod address_details;
mod button;
mod dialog;
mod fido;
@ -7,6 +8,7 @@ mod frame;
mod hold_to_confirm;
#[cfg(feature = "dma2d")]
mod homescreen;
mod horizontal_page;
mod keyboard;
mod loader;
mod number_input;
@ -17,9 +19,10 @@ mod scroll;
mod swipe;
mod welcome_screen;
pub use address_details::AddressDetails;
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg, IconText, SelectWordMsg,
CancelInfoConfirmMsg, FloatingButton, FloatingButtonMsg, IconText, SelectWordMsg,
};
pub use dialog::{Dialog, DialogMsg, IconDialog};
pub use fido::{FidoConfirm, FidoMsg};
@ -27,6 +30,7 @@ pub use frame::{Frame, NotificationFrame};
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
#[cfg(feature = "dma2d")]
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
pub use horizontal_page::HorizontalPage;
pub use keyboard::{
bip39::Bip39Input,
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},

View File

@ -1,7 +1,8 @@
use crate::ui::{
component::{
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, FixedHeightBar, Label,
Pad, Paginate,
base::ComponentExt,
paginated::{AuxPageMsg, PageMsg},
Component, Event, EventCtx, FixedHeightBar, Label, Pad, Paginate,
},
display::{self, toif::Icon, Color},
geometry::{Insets, Rect},
@ -21,6 +22,7 @@ pub struct SwipePage<T, U> {
scrollbar: ScrollBar,
hint: Label<&'static str>,
button_back: Option<Button<&'static str>>,
swipe_left: bool,
fade: Option<u16>,
}
@ -39,6 +41,7 @@ where
pad: Pad::with_background(background),
hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()),
button_back: None,
swipe_left: false,
fade: None,
}
}
@ -48,9 +51,15 @@ where
self
}
pub fn with_swipe_left(mut self) -> Self {
self.swipe_left = true;
self
}
fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
self.swipe.allow_left = self.swipe_left;
}
fn on_page_change(&mut self, ctx: &mut EventCtx) {
@ -138,6 +147,9 @@ where
self.on_page_change(ctx);
return None;
}
SwipeDirection::Left if self.swipe_left => {
return Some(PageMsg::Aux(AuxPageMsg::SwipeLeft));
}
_ => {
// Ignore other directions.
}
@ -152,7 +164,7 @@ where
}
} else {
if let Some(ButtonMsg::Clicked) = self.button_back.event(ctx, event) {
return Some(PageMsg::GoBack);
return Some(PageMsg::Aux(AuxPageMsg::GoBack));
}
self.hint.event(ctx, event);
}

View File

@ -18,7 +18,7 @@ use crate::{
component::{
base::ComponentExt,
image::BlendedImage,
paginated::{PageMsg, Paginate},
paginated::{AuxPageMsg, PageMsg, Paginate},
painter,
placed::GridPlaced,
text::{
@ -45,9 +45,10 @@ use crate::{
use super::{
component::{
Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg,
Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, HoldToConfirm, HoldToConfirmMsg,
Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard,
AddressDetails, Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg, Dialog, DialogMsg, FidoConfirm, FidoMsg, FloatingButton,
FloatingButtonMsg, Frame, HoldToConfirm, HoldToConfirmMsg, Homescreen, HomescreenMsg,
HorizontalPage, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard,
MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog, NumberInputDialogMsg,
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input, SwipeHoldPage, SwipePage,
@ -223,7 +224,8 @@ where
match msg {
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(msg) => msg.try_into(),
PageMsg::GoBack => Ok(CANCELLED.as_obj()),
PageMsg::Aux(AuxPageMsg::GoBack) => Ok(CANCELLED.as_obj()),
PageMsg::Aux(AuxPageMsg::SwipeLeft) => Ok(INFO.as_obj()),
}
}
}
@ -236,7 +238,7 @@ where
match msg {
PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(msg) => msg.try_into(),
PageMsg::GoBack => unreachable!(),
PageMsg::Aux(_) => Err(Error::TypeError),
}
}
}
@ -338,6 +340,41 @@ impl ComponentMsgObj for Qr {
}
}
impl<T> ComponentMsgObj for FloatingButton<T>
where
T: ComponentMsgObj,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
FloatingButtonMsg::ButtonClicked => Ok(INFO.as_obj()),
FloatingButtonMsg::Content(c) => self.inner().msg_try_into_obj(c),
}
}
}
impl<T> ComponentMsgObj for HorizontalPage<T>
where
T: ComponentMsgObj + Paginate,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
PageMsg::Content(inner_msg) => Ok(self.inner().msg_try_into_obj(inner_msg)?),
PageMsg::Controls(_) => Err(Error::TypeError),
PageMsg::Aux(AuxPageMsg::GoBack) => Ok(CANCELLED.as_obj()),
PageMsg::Aux(_) => Err(Error::TypeError),
}
}
}
impl<T> ComponentMsgObj for AddressDetails<T>
where
T: ParagraphStrType,
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!();
}
}
extern "C" fn new_confirm_action(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()?;
@ -434,23 +471,54 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map
let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
let verb: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let verb: StrBuffer = "CONFIRM".into();
confirm_blob(title, data, description, extra, verb, verb_cancel, hold)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
confirm_blob(
title,
data,
description,
extra,
Some(verb),
verb_cancel,
hold,
)
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 description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
let paragraphs = ConfirmBlob {
description: description.unwrap_or_else(StrBuffer::empty),
extra: extra.unwrap_or_else(StrBuffer::empty),
data: data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_BOLD,
data_font: &theme::TEXT_MONO,
}
.into_paragraphs();
let buttons = Button::cancel_confirm(
Button::<&'static str>::with_icon(Icon::new(theme::ICON_CANCEL)),
Button::<&'static str>::with_icon(Icon::new(theme::ICON_CONFIRM))
.styled(theme::button_confirm()),
1,
);
let obj = LayoutObj::new(FloatingButton::top_right_corner(
Icon::new(theme::ICON_INFO_CIRCLE),
Frame::left_aligned(
theme::label_title(),
title,
SwipePage::new(paragraphs, buttons, theme::BG).with_swipe_left(),
),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -583,6 +651,36 @@ extern "C" fn new_show_qr(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_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)?;
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(
HorizontalPage::new(
FloatingButton::top_right_corner(Icon::new(theme::ICON_CANCEL_LARGER), ad),
theme::BG,
)
.with_swipe_right_to_go_back(),
)?;
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()?;
@ -847,6 +945,33 @@ 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 title: StrBuffer = "Address mismatch?".into();
let description: StrBuffer = "Please contact Trezor support at".into();
let url: StrBuffer = "trezor.io/support".into();
let button = "QUIT";
let icon = BlendedImage::single(Icon::new(theme::ICON_OCTA), theme::WARN_COLOR, theme::BG);
let obj = LayoutObj::new(
IconDialog::new(
icon,
title,
Button::cancel_confirm_square(
Button::with_icon(Icon::new(theme::ICON_BACK)),
Button::with_text(button).styled(theme::button_reset()),
),
)
.with_description(description)
.with_text(&theme::TEXT_DEMIBOLD, url),
)?;
Ok(obj.into())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
@ -1422,12 +1547,24 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// data: str | bytes,
/// description: str | None,
/// extra: str | None,
/// verb: str | None = None,
/// verb_cancel: str | None = None,
/// hold: bool = False,
/// ) -> object:
/// """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,
/// extra: str | None,
/// ) -> object:
/// """Confirm address. Similar to `confirm_blob` but has corner info button
/// and allows left swipe which does the same thing as the button."""
Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(),
/// def confirm_properties(
/// *,
/// title: str,
@ -1456,6 +1593,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Show QR code."""
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).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,
@ -1551,6 +1699,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Info modal. No buttons shown when `button` is empty string."""
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 show_simple(
/// *,
/// title: str | None,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -55,6 +55,7 @@ pub const ICON_SIZE: i16 = 16;
// UI icons (greyscale).
pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
pub const ICON_CANCEL_LARGER: &[u8] = include_res!("model_tt/res/cancel-larger.toif");
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.toif");
pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif");
pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif");
@ -70,6 +71,8 @@ pub const ICON_SUCCESS_SMALL: &[u8] = include_res!("model_tt/res/success_bld.toi
pub const ICON_WARN_SMALL: &[u8] = include_res!("model_tt/res/warn_bld.toif");
pub const ICON_PAGE_NEXT: &[u8] = include_res!("model_tt/res/page-next.toif");
pub const ICON_PAGE_PREV: &[u8] = include_res!("model_tt/res/page-prev.toif");
pub const ICON_OCTA: &[u8] = include_res!("model_tt/res/octa-bang.toif");
pub const ICON_INFO_CIRCLE: &[u8] = include_res!("model_tt/res/info-circle.toif");
// Large, three-color icons.
pub const WARN_COLOR: Color = YELLOW;
@ -139,7 +142,7 @@ pub const fn label_title() -> TextStyle {
TextStyle::new(Font::BOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
}
pub fn button_default() -> ButtonStyleSheet {
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -171,7 +174,7 @@ pub fn button_default() -> ButtonStyleSheet {
}
}
pub fn button_confirm() -> ButtonStyleSheet {
pub const fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -203,7 +206,7 @@ pub fn button_confirm() -> ButtonStyleSheet {
}
}
pub fn button_cancel() -> ButtonStyleSheet {
pub const fn button_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -235,11 +238,11 @@ pub fn button_cancel() -> ButtonStyleSheet {
}
}
pub fn button_danger() -> ButtonStyleSheet {
pub const fn button_danger() -> ButtonStyleSheet {
button_cancel()
}
pub fn button_reset() -> ButtonStyleSheet {
pub const fn button_reset() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -271,7 +274,7 @@ pub fn button_reset() -> ButtonStyleSheet {
}
}
pub fn button_info() -> ButtonStyleSheet {
pub const fn button_info() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
@ -303,7 +306,7 @@ pub fn button_info() -> ButtonStyleSheet {
}
}
pub fn button_pin() -> ButtonStyleSheet {
pub const fn button_pin() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::MONO,
@ -335,7 +338,7 @@ pub fn button_pin() -> ButtonStyleSheet {
}
}
pub fn button_counter() -> ButtonStyleSheet {
pub const fn button_counter() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::DEMIBOLD,
@ -367,11 +370,11 @@ pub fn button_counter() -> ButtonStyleSheet {
}
}
pub fn button_clear() -> ButtonStyleSheet {
pub const fn button_clear() -> ButtonStyleSheet {
button_default()
}
pub fn loader_default() -> LoaderStyleSheet {
pub const fn loader_default() -> LoaderStyleSheet {
LoaderStyleSheet {
normal: &LoaderStyle {
icon: None,
@ -418,6 +421,7 @@ pub const TEXT_CHECKLIST_SELECTED: TextStyle =
TextStyle::new(Font::NORMAL, FG, BG, GREY_LIGHT, GREY_LIGHT);
pub const TEXT_CHECKLIST_DONE: TextStyle =
TextStyle::new(Font::NORMAL, GREEN_DARK, BG, GREY_LIGHT, GREY_LIGHT);
pub const TEXT_XPUB: TextStyle = TEXT_NORMAL.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
pub const FORMATTED: FormattedFonts = FormattedFonts {
normal: Font::NORMAL,
@ -432,6 +436,8 @@ pub const BUTTON_HEIGHT: i16 = 38;
pub const BUTTON_SPACING: i16 = 6;
pub const CHECKLIST_SPACING: i16 = 10;
pub const RECOVERY_SPACING: i16 = 18;
pub const CORNER_BUTTON_SIDE: i16 = 32;
pub const CORNER_BUTTON_SPACING: i16 = 8;
/// Standard button height in pixels.
pub const fn button_rows(count: usize) -> i16 {
@ -462,6 +468,10 @@ pub const fn borders_scroll() -> Insets {
Insets::new(13, 5, 14, 10)
}
pub const fn borders_horizontal_scroll() -> Insets {
Insets::new(13, 10, 0, 10)
}
pub const fn borders_notification() -> Insets {
Insets::new(48, 10, 14, 10)
}

View File

@ -82,12 +82,25 @@ def confirm_blob(
data: str | bytes,
description: str | None,
extra: str | None,
verb: str | None = None,
verb_cancel: str | None = None,
hold: bool = False,
) -> object:
"""Confirm byte sequence data."""
# rust/src/ui/model_tt/layout.rs
def confirm_address(
*,
title: str,
data: str | bytes,
description: str | None,
extra: str | None,
) -> object:
"""Confirm address. Similar to `confirm_blob` but has corner info button
and allows left swipe which does the same thing as the button."""
# rust/src/ui/model_tt/layout.rs
def confirm_properties(
*,
@ -119,6 +132,18 @@ def show_qr(
"""Show QR code."""
# rust/src/ui/model_tt/layout.rs
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."""
# rust/src/ui/model_tt/layout.rs
def confirm_value(
*,
@ -222,6 +247,11 @@ def show_info(
"""Info modal. No buttons shown when `button` is empty string."""
# rust/src/ui/model_tt/layout.rs
def show_mismatch() -> object:
"""Warning modal, receiving address mismatch."""
# rust/src/ui/model_tt/layout.rs
def show_simple(
*,

View File

@ -28,7 +28,6 @@ async def get_address(
pubkey = node.public_key()
address = address_from_public_key(pubkey, HRP)
if msg.show_display:
title = paths.address_n_to_str(address_n)
await show_address(ctx, address, title=title)
await show_address(ctx, address, path=paths.address_n_to_str(address_n))
return BinanceAddress(address=address)

View File

@ -41,7 +41,7 @@ async def get_address(
from apps.common.paths import address_n_to_str, validate_path
from . import addresses
from .keychain import validate_path_against_script_type
from .keychain import address_n_to_name, validate_path_against_script_type
from .multisig import multisig_pubkey_index
multisig = msg.multisig # local_cache_attribute
@ -95,6 +95,7 @@ async def get_address(
mac = get_address_mac(address, coin.slip44, keychain)
if msg.show_display:
path = address_n_to_str(address_n)
if multisig:
if multisig.nodes:
pubnodes = multisig.nodes
@ -102,23 +103,30 @@ async def get_address(
pubnodes = [hd.node for hd in multisig.pubkeys]
multisig_index = multisig_pubkey_index(multisig, node.public_key())
title = f"Multisig {multisig.m} of {len(pubnodes)}"
await show_address(
ctx,
address_short,
case_sensitive=address_case_sensitive,
title=title,
path=path,
multisig_index=multisig_index,
xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes),
account=f"Multisig {multisig.m} of {len(pubnodes)}",
)
else:
title = address_n_to_str(address_n)
account_name = address_n_to_name(coin, address_n, script_type)
if account_name is None:
account = "Unknown path"
elif account_name == "":
account = coin.coin_shortcut
else:
account = f"{coin.coin_shortcut} {account_name}"
await show_address(
ctx,
address_short,
address_qr=address,
case_sensitive=address_case_sensitive,
title=title,
path=path,
account=account,
)
return Address(address=address, mac=mac)

View File

@ -384,7 +384,7 @@ def address_n_to_name(
) -> str | None:
ACCOUNT_TYPES = (
AccountType(
"Legacy account",
"Legacy",
PATTERN_BIP44,
InputScriptType.SPENDADDRESS,
require_segwit=True,
@ -392,7 +392,7 @@ def address_n_to_name(
require_taproot=False,
),
AccountType(
"Account",
"",
PATTERN_BIP44,
InputScriptType.SPENDADDRESS,
require_segwit=False,
@ -400,7 +400,7 @@ def address_n_to_name(
require_taproot=False,
),
AccountType(
"Legacy SegWit account",
"L. SegWit",
PATTERN_BIP49,
InputScriptType.SPENDP2SHWITNESS,
require_segwit=True,
@ -408,7 +408,7 @@ def address_n_to_name(
require_taproot=False,
),
AccountType(
"SegWit account",
"SegWit",
PATTERN_BIP84,
InputScriptType.SPENDWITNESS,
require_segwit=True,
@ -416,7 +416,7 @@ def address_n_to_name(
require_taproot=False,
),
AccountType(
"Taproot account",
"Taproot",
PATTERN_BIP86,
InputScriptType.SPENDTAPROOT,
require_segwit=False,
@ -424,7 +424,7 @@ def address_n_to_name(
require_taproot=True,
),
AccountType(
"Coinjoin account",
"Coinjoin",
PATTERN_SLIP25_TAPROOT,
InputScriptType.SPENDTAPROOT,
require_segwit=False,

View File

@ -975,9 +975,8 @@ async def show_cardano_address(
if not protocol_magics.is_mainnet(protocol_magic):
network_name = protocol_magics.to_ui_string(protocol_magic)
title = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]} address"
address_extra = None
title_qr = title
path = None
account = ADDRESS_TYPE_NAMES[address_parameters.address_type]
if address_parameters.address_type in (
CAT.BYRON,
CAT.BASE,
@ -987,17 +986,14 @@ async def show_cardano_address(
CAT.REWARD,
):
if address_parameters.address_n:
address_extra = address_n_to_str(address_parameters.address_n)
title_qr = address_n_to_str(address_parameters.address_n)
path = address_n_to_str(address_parameters.address_n)
elif address_parameters.address_n_staking:
address_extra = address_n_to_str(address_parameters.address_n_staking)
title_qr = address_n_to_str(address_parameters.address_n_staking)
path = address_n_to_str(address_parameters.address_n_staking)
await layouts.show_address(
ctx,
address,
title=title,
path=path,
account=account,
network=network_name,
address_extra=address_extra,
title_qr=title_qr,
)

View File

@ -32,7 +32,6 @@ async def get_address(
address = address_from_bytes(node.ethereum_pubkeyhash(), network)
if msg.show_display:
title = paths.address_n_to_str(address_n)
await show_address(ctx, address, title=title)
await show_address(ctx, address, path=paths.address_n_to_str(address_n))
return EthereumAddress(address=address)

View File

@ -68,12 +68,11 @@ async def get_address(
)
if msg.show_display:
title = paths.address_n_to_str(msg.address_n)
await show_address(
ctx,
addr,
address_qr="monero:" + addr,
title=title,
path=paths.address_n_to_str(msg.address_n),
)
return MoneroAddress(address=addr.encode())

View File

@ -30,12 +30,11 @@ async def get_address(
address = node.nem_address(network)
if msg.show_display:
title = address_n_to_str(address_n)
await show_address(
ctx,
address,
case_sensitive=False,
title=title,
path=address_n_to_str(address_n),
network=get_network_str(network),
)

View File

@ -25,7 +25,6 @@ async def get_address(
address = address_from_public_key(pubkey)
if msg.show_display:
title = paths.address_n_to_str(msg.address_n)
await show_address(ctx, address, title=title)
await show_address(ctx, address, path=paths.address_n_to_str(msg.address_n))
return RippleAddress(address=address)

View File

@ -24,7 +24,7 @@ async def get_address(
address = helpers.address_from_public_key(pubkey)
if msg.show_display:
title = paths.address_n_to_str(msg.address_n)
await show_address(ctx, address, case_sensitive=False, title=title)
path = paths.address_n_to_str(msg.address_n)
await show_address(ctx, address, case_sensitive=False, path=path)
return StellarAddress(address=address)

View File

@ -29,7 +29,6 @@ async def get_address(
address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX)
if msg.show_display:
title = paths.address_n_to_str(msg.address_n)
await show_address(ctx, address, title=title)
await show_address(ctx, address, path=paths.address_n_to_str(msg.address_n))
return TezosAddress(address=address)

View File

@ -164,8 +164,6 @@ async def show_address(
network: str | None = None,
multisig_index: int | None = None,
xpubs: Sequence[str] = (),
address_extra: str | None = None,
title_qr: str | None = None,
) -> None:
result = await interact(
ctx,

View File

@ -353,24 +353,20 @@ async def confirm_homescreen(
)
def _show_xpub(xpub: str, title: str, cancel: str | None) -> ui.Layout:
content = RustLayout(
trezorui2.confirm_blob(
title=title,
data=xpub,
verb_cancel=cancel,
extra=None,
description=None,
)
)
return content
async def show_xpub(ctx: GenericContext, xpub: str, title: str) -> None:
await raise_if_not_confirmed(
interact(
ctx,
_show_xpub(xpub, title, None),
RustLayout(
trezorui2.confirm_blob(
title=title,
data=xpub,
verb="CONFIRM",
verb_cancel=None,
extra=None,
description=None,
)
),
"show_xpub",
ButtonRequestType.PublicKey,
)
@ -383,61 +379,72 @@ async def show_address(
*,
address_qr: str | None = None,
case_sensitive: bool = True,
title: str = "Confirm address",
path: str | None = None,
account: str | None = None,
network: str | None = None,
multisig_index: int | None = None,
xpubs: Sequence[str] = (),
address_extra: str | None = None,
title_qr: str | None = None,
) -> None:
is_multisig = len(xpubs) > 0
while True:
title = (
"RECEIVE ADDRESS\n(MULTISIG)"
if multisig_index is not None
else "RECEIVE ADDRESS"
)
result = await interact(
ctx,
RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
trezorui2.confirm_address(
title=title,
data=address,
description=network or "",
extra=address_extra or "",
verb_cancel="QR",
extra=None,
)
),
"show_address",
ButtonRequestType.Address,
)
# User pressed right button.
if result is CONFIRMED:
break
result = await interact(
ctx,
RustLayout(
trezorui2.show_qr(
address=address if address_qr is None else address_qr,
case_sensitive=case_sensitive,
title=title.upper() if title_qr is None else title_qr.upper(),
verb_cancel="XPUBs" if is_multisig else "ADDRESS",
)
),
"show_qr",
ButtonRequestType.Address,
)
if result is CONFIRMED:
break
# User pressed corner button or swiped left, go to address details.
elif result is INFO:
if is_multisig:
for i, xpub in enumerate(xpubs):
cancel = "NEXT" if i < len(xpubs) - 1 else "ADDRESS"
title_xpub = f"XPUB #{i + 1}"
title_xpub += " (yours)" if i == multisig_index else " (cosigner)"
result = await interact(
ctx,
_show_xpub(xpub, title=title_xpub, cancel=cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
if result is CONFIRMED:
return
def xpub_title(i: int):
result = f"MULTISIG XPUB #{i + 1}\n"
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 but corner button returns INFO.
assert result in (INFO, CANCELLED)
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(
@ -693,6 +700,7 @@ async def confirm_blob(
data=data,
extra=None,
hold=hold,
verb="CONFIRM",
)
)

View File

@ -129,9 +129,11 @@ class LayoutContent:
# First line should have content after the tag, last line does not store content
tag = f"< {tag_name}"
if tag in self.lines[0]:
first_line = self.lines[0].split(tag)[1]
all_lines = [first_line] + self.lines[1:-1]
for i in range(len(self.lines)):
if tag in self.lines[i]:
first_line = self.lines[i].split(tag)[1]
all_lines = [first_line] + self.lines[i + 1 : -1]
break
else:
all_lines = self.lines[1:-1]

View File

@ -18,7 +18,7 @@ import pytest
from trezorlib import btc, messages, tools
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import TrezorFailure
from trezorlib.exceptions import Cancelled, TrezorFailure
VECTORS = ( # path, script_type, address
(
@ -43,9 +43,12 @@ VECTORS = ( # path, script_type, address
),
)
CORNER_BUTTON = (215, 25)
@pytest.mark.skip_t2
@pytest.mark.parametrize("path, script_type, address", VECTORS)
def test_show(
def test_show_t1(
client: Client, path: str, script_type: messages.InputScriptType, address: str
):
def input_flow():
@ -68,6 +71,67 @@ def test_show(
)
@pytest.mark.skip_t1
@pytest.mark.parametrize("path, script_type, address", VECTORS)
def test_show_tt(
client: Client, path: str, script_type: messages.InputScriptType, address: str
):
def input_flow():
yield
client.debug.click(CORNER_BUTTON, wait=True)
yield
client.debug.swipe_left(wait=True)
client.debug.swipe_right(wait=True)
client.debug.swipe_left(wait=True)
client.debug.click(CORNER_BUTTON, wait=True)
yield
client.debug.press_no()
yield
client.debug.press_no()
yield
client.debug.press_yes()
with client:
client.set_input_flow(input_flow)
assert (
btc.get_address(
client,
"Bitcoin",
tools.parse_path(path),
script_type=script_type,
show_display=True,
)
== address
)
@pytest.mark.skip_t1
@pytest.mark.parametrize("path, script_type, address", VECTORS)
def test_show_cancel(
client: Client, path: str, script_type: messages.InputScriptType, address: str
):
def input_flow():
yield
client.debug.click(CORNER_BUTTON, wait=True)
yield
client.debug.swipe_left(wait=True)
client.debug.click(CORNER_BUTTON, wait=True)
yield
client.debug.press_no()
yield
client.debug.press_yes()
with client, pytest.raises(Cancelled):
client.set_input_flow(input_flow)
btc.get_address(
client,
"Bitcoin",
tools.parse_path(path),
script_type=script_type,
show_display=True,
)
def test_show_unrecognized_path(client: Client):
with pytest.raises(TrezorFailure):
btc.get_address(
@ -213,32 +277,36 @@ def test_show_multisig_xpubs(
def input_flow():
yield # show address
layout = client.debug.wait_layout() # TODO: do not need to *wait* now?
assert layout.get_title() == "MULTISIG 2 OF 3"
assert layout.get_title() == "RECEIVE ADDRESS (MULTISIG)"
assert layout.get_content().replace(" ", "") == address
client.debug.press_no()
client.debug.click(CORNER_BUTTON)
yield # show QR code
assert "Painter" in client.debug.wait_layout().text
assert "Qr" in client.debug.wait_layout().text
client.debug.swipe_left()
# address details
layout = client.debug.wait_layout()
assert "Multisig 2 of 3" in layout.text
# Three xpub pages with the same testing logic
for xpub_num in range(3):
expected_title = f"XPUB #{xpub_num + 1} " + (
"(yours)" if i == xpub_num else "(cosigner)"
expected_title = f"MULTISIG XPUB #{xpub_num + 1} " + (
"(YOURS)" if i == xpub_num else "(COSIGNER)"
)
client.debug.press_no()
yield # show XPUB#{xpub_num}
layout1 = client.debug.wait_layout()
assert layout1.get_title() == expected_title
client.debug.swipe_up()
layout2 = client.debug.wait_layout()
assert layout2.get_title() == expected_title
content = (layout1.get_content() + layout2.get_content()).replace(
" ", ""
)
assert content == xpubs[xpub_num]
client.debug.swipe_left()
layout = client.debug.wait_layout()
assert layout.get_title() == expected_title
content = layout.get_content().replace(" ", "")
assert xpubs[xpub_num] in content
client.debug.click(CORNER_BUTTON)
yield # show address
client.debug.press_no()
yield # address mismatch
client.debug.press_no()
yield # show address
client.debug.press_yes()
with client:

View File

@ -340,7 +340,7 @@ def test_signmessage_pagination(client: Client, message: str):
expected_message = (
("Confirm message: " + message).replace("\n", "").replace(" ", "")
)
message_read = message_read.replace(" ", "")
message_read = message_read.replace(" ", "").replace("...", "")
assert expected_message == message_read

File diff suppressed because it is too large Load Diff