feat(core/rust/ui): bitcoin layouts

[no changelog]
pull/2357/head
Martin Milata 2 years ago
parent dd9a7d30e5
commit c9ca7cd544

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -258,6 +258,7 @@ fn generate_trezorhal_bindings() {
.allowlist_function("display_bar") .allowlist_function("display_bar")
.allowlist_function("display_bar_radius") .allowlist_function("display_bar_radius")
.allowlist_function("display_icon") .allowlist_function("display_icon")
.allowlist_function("display_image")
.allowlist_function("display_toif_info") .allowlist_function("display_toif_info")
.allowlist_function("display_loader") .allowlist_function("display_loader")
.allowlist_function("display_pixeldata") .allowlist_function("display_pixeldata")

@ -17,7 +17,18 @@ static void _librust_qstrs(void) {
MP_QSTR_CANCELLED; MP_QSTR_CANCELLED;
MP_QSTR_INFO; MP_QSTR_INFO;
MP_QSTR_confirm_action; MP_QSTR_confirm_action;
MP_QSTR_confirm_blob;
MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_joint_total;
MP_QSTR_confirm_modify_fee;
MP_QSTR_confirm_modify_output;
MP_QSTR_confirm_output;
MP_QSTR_confirm_payment_request;
MP_QSTR_confirm_text; MP_QSTR_confirm_text;
MP_QSTR_confirm_total;
MP_QSTR_show_qr;
MP_QSTR_show_success;
MP_QSTR_show_warning;
MP_QSTR_request_pin; MP_QSTR_request_pin;
MP_QSTR_request_passphrase; MP_QSTR_request_passphrase;
MP_QSTR_request_bip39; MP_QSTR_request_bip39;
@ -33,14 +44,28 @@ static void _librust_qstrs(void) {
MP_QSTR_page_count; MP_QSTR_page_count;
MP_QSTR_title; MP_QSTR_title;
MP_QSTR_subtitle;
MP_QSTR_action; MP_QSTR_action;
MP_QSTR_description; MP_QSTR_description;
MP_QSTR_extra;
MP_QSTR_verb; MP_QSTR_verb;
MP_QSTR_verb_cancel; MP_QSTR_verb_cancel;
MP_QSTR_hold;
MP_QSTR_reverse; MP_QSTR_reverse;
MP_QSTR_prompt; MP_QSTR_prompt;
MP_QSTR_subprompt; MP_QSTR_subprompt;
MP_QSTR_warning; MP_QSTR_warning;
MP_QSTR_allow_cancel; MP_QSTR_allow_cancel;
MP_QSTR_max_len; MP_QSTR_max_len;
MP_QSTR_amount_change;
MP_QSTR_amount_new;
MP_QSTR_ask_pagination;
MP_QSTR_case_sensitive;
MP_QSTR_coin_name;
MP_QSTR_max_feerate;
MP_QSTR_max_rounds;
MP_QSTR_spending_amount;
MP_QSTR_total_amount;
MP_QSTR_total_fee_new;
MP_QSTR_user_fee_change;
} }

@ -143,6 +143,12 @@ impl AsMut<[u8]> for BufferMut {
#[derive(Default)] #[derive(Default)]
pub struct StrBuffer(Buffer); pub struct StrBuffer(Buffer);
impl StrBuffer {
pub fn empty() -> Self {
Self::from("")
}
}
impl TryFrom<Obj> for StrBuffer { impl TryFrom<Obj> for StrBuffer {
type Error = Error; type Error = Error;

@ -100,6 +100,18 @@ impl Map {
} }
} }
pub fn get_or<T>(&self, index: impl Into<Obj>, default: T) -> Result<T, Error>
where
T: TryFrom<Obj, Error = Error>,
{
let res = self.get(index);
match res {
Ok(obj) => obj.try_into(),
Err(Error::KeyError(_)) => Ok(default),
Err(e) => Err(e),
}
}
pub fn set(&mut self, index: impl Into<Obj>, value: impl Into<Obj>) -> Result<(), Error> { pub fn set(&mut self, index: impl Into<Obj>, value: impl Into<Obj>) -> Result<(), Error> {
self.set_obj(index.into(), value.into()) self.set_obj(index.into(), value.into())
} }

@ -62,6 +62,10 @@ pub fn icon(x: i32, y: i32, w: i32, h: i32, data: &[u8], fgcolor: u16, bgcolor:
} }
} }
pub fn image(x: i32, y: i32, w: i32, h: i32, data: &[u8]) {
unsafe { ffi::display_image(x, y, w, h, data.as_ptr() as _, data.len() as _) }
}
pub fn toif_info(data: &[u8]) -> Result<ToifInfo, ()> { pub fn toif_info(data: &[u8]) -> Result<ToifInfo, ()> {
let mut width: cty::uint16_t = 0; let mut width: cty::uint16_t = 0;
let mut height: cty::uint16_t = 0; let mut height: cty::uint16_t = 0;

@ -3,6 +3,7 @@ pub mod common;
#[cfg(feature = "ui")] #[cfg(feature = "ui")]
pub mod display; pub mod display;
mod ffi; mod ffi;
pub mod qr;
pub mod random; pub mod random;
#[cfg(feature = "model_tr")] #[cfg(feature = "model_tr")]
pub mod rgb_led; pub mod rgb_led;

@ -0,0 +1,75 @@
use crate::error::Error;
use cstr_core::CStr;
extern "C" {
fn display_qrcode(
x: cty::c_int,
y: cty::c_int,
data: *const cty::uint16_t,
scale: cty::uint8_t,
);
}
const NVERSIONS: usize = 10; // range of versions (=capacities) that we support
const QR_WIDTHS: [u32; NVERSIONS] = [21, 25, 29, 33, 37, 41, 45, 49, 53, 57];
const THRESHOLDS_BINARY: [usize; NVERSIONS] = [14, 26, 42, 62, 84, 106, 122, 152, 180, 213];
const THRESHOLDS_ALPHANUM: [usize; NVERSIONS] = [20, 38, 61, 90, 122, 154, 178, 221, 262, 311];
const ALPHANUM: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $*+-./:";
const MAX_DATA: usize = THRESHOLDS_ALPHANUM[THRESHOLDS_ALPHANUM.len() - 1] + 1; //FIXME
fn is_alphanum_only(data: &str) -> bool {
data.chars().all(|c| ALPHANUM.contains(c))
}
fn qr_version_index(data: &str, thresholds: &[usize]) -> Option<usize> {
for (i, threshold) in thresholds.iter().enumerate() {
if data.len() <= *threshold {
return Some(i);
}
}
None
}
pub fn render_qrcode(
x: i32,
y: i32,
data: &str,
max_size: u32,
case_sensitive: bool,
) -> Result<(), Error> {
let data_len = data.len();
let version_idx;
let mut buffer = [0u8; MAX_DATA];
assert!(data_len < buffer.len());
buffer.as_mut_slice()[..data_len].copy_from_slice(data.as_bytes());
if case_sensitive && !is_alphanum_only(data) {
version_idx = match qr_version_index(data, &THRESHOLDS_BINARY) {
Some(idx) => idx,
_ => return Err(Error::OutOfRange),
}
} else if let Some(idx) = qr_version_index(data, &THRESHOLDS_ALPHANUM) {
version_idx = idx;
if data_len > THRESHOLDS_BINARY[idx] {
for c in buffer.iter_mut() {
c.make_ascii_uppercase()
}
};
} else {
return Err(Error::OutOfRange);
}
let size = QR_WIDTHS[version_idx];
let scale = max_size / size;
assert!((1..=10).contains(&scale));
unsafe {
buffer[data_len] = 0u8;
let cstr = CStr::from_bytes_with_nul_unchecked(&buffer[..data_len + 1]);
display_qrcode(x, y, cstr.as_ptr() as _, scale as u8);
Ok(())
}
}

@ -7,7 +7,7 @@ use crate::{
ui::{ ui::{
component::{maybe::PaintOverlapping, Map}, component::{maybe::PaintOverlapping, Map},
display::Color, display::Color,
geometry::Rect, geometry::{Offset, Rect},
}, },
}; };
@ -210,6 +210,60 @@ where
} }
} }
impl<M, T, U, V> Component for (T, U, V)
where
T: Component<Msg = M>,
U: Component<Msg = M>,
V: Component<Msg = M>,
{
type Msg = M;
fn place(&mut self, bounds: Rect) -> Rect {
self.0
.place(bounds)
.union(self.1.place(bounds))
.union(self.2.place(bounds))
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.0
.event(ctx, event)
.or_else(|| self.1.event(ctx, event))
.or_else(|| self.2.event(ctx, event))
}
fn paint(&mut self) {
self.0.paint();
self.1.paint();
self.2.paint();
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.0.bounds(sink);
self.1.bounds(sink);
self.2.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T, U, V> crate::trace::Trace for (T, U, V)
where
T: Component,
T: crate::trace::Trace,
U: Component,
U: crate::trace::Trace,
V: Component,
V: crate::trace::Trace,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Tuple");
d.field("0", &self.0);
d.field("1", &self.1);
d.field("2", &self.2);
d.close();
}
}
impl<T> Component for Option<T> impl<T> Component for Option<T>
where where
T: Component, T: Component,

@ -0,0 +1,42 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display,
geometry::Rect,
};
pub struct Image {
image: &'static [u8],
area: Rect,
}
impl Image {
pub fn new(image: &'static [u8]) -> Self {
Self {
image,
area: Rect::zero(),
}
}
}
impl Component for Image {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
display::image(self.area.center(), self.image)
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
if let Some((size, _)) = display::toif_info(self.image) {
sink(Rect::from_center_and_size(self.area.center(), size));
}
}
}

@ -92,3 +92,13 @@ where
sink(self.area) sink(self.area)
} }
} }
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Label<T>
where
T: Deref<Target = str>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.string(&self.text)
}
}

@ -2,6 +2,7 @@
pub mod base; pub mod base;
pub mod empty; pub mod empty;
pub mod image;
pub mod label; pub mod label;
pub mod map; pub mod map;
pub mod maybe; pub mod maybe;
@ -13,12 +14,13 @@ pub mod text;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use empty::Empty; pub use empty::Empty;
pub use image::Image;
pub use label::{Label, LabelStyle}; pub use label::{Label, LabelStyle};
pub use map::Map; pub use map::Map;
pub use maybe::Maybe; pub use maybe::Maybe;
pub use pad::Pad; pub use pad::Pad;
pub use paginated::{PageMsg, Paginate}; pub use paginated::{PageMsg, Paginate};
pub use painter::Painter; pub use painter::{qrcode_painter, Painter};
pub use placed::GridPlaced; pub use placed::GridPlaced;
pub use text::{ pub use text::{
formatted::FormattedText, formatted::FormattedText,

@ -1,5 +1,6 @@
use crate::ui::{ use crate::ui::{
component::{Component, Event, EventCtx, Never}, component::{Component, Event, EventCtx, Never},
display,
geometry::Rect, geometry::Rect,
}; };
@ -35,4 +36,31 @@ where
fn paint(&mut self) { fn paint(&mut self) {
(self.func)(self.area); (self.func)(self.area);
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
}
}
#[cfg(feature = "ui_debug")]
impl<F> crate::trace::Trace for Painter<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.string("Painter")
}
}
pub fn qrcode_painter<T>(data: T, max_size: u32, case_sensitive: bool) -> Painter<impl FnMut(Rect)>
where
T: AsRef<str>,
{
// Ignore errors as we currently can't propagate them out of paint().
let f = move |area: Rect| {
display::qrcode(area.center(), data.as_ref(), max_size, case_sensitive).unwrap_or(())
};
Painter::new(f)
}
pub fn image_painter(image: &'static [u8]) -> Painter<impl FnMut(Rect)> {
let f = move |area: Rect| display::image(area.center(), image);
Painter::new(f)
} }

@ -1,7 +1,8 @@
use super::constant; use super::constant;
use crate::{ use crate::{
error::Error,
time::Duration, time::Duration,
trezorhal::{display, time}, trezorhal::{display, qr, time},
}; };
use super::geometry::{Offset, Point, Rect}; use super::geometry::{Offset, Point, Rect};
@ -91,6 +92,34 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) {
); );
} }
pub fn image(center: Point, data: &[u8]) {
let toif_info = display::toif_info(data).unwrap();
assert!(!toif_info.grayscale);
let r = Rect::from_center_and_size(
center,
Offset::new(toif_info.width.into(), toif_info.height.into()),
);
display::image(
r.x0,
r.y0,
r.width(),
r.height(),
&data[12..], // Skip TOIF header.
);
}
pub fn toif_info(data: &[u8]) -> Option<(Offset, bool)> {
if let Ok(info) = display::toif_info(data) {
Some((
Offset::new(info.width.into(), info.height.into()),
info.grayscale,
))
} else {
None
}
}
// Used on T1 only. // Used on T1 only.
pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) { pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) {
display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into()); display::bar(r.x0, r.y0, r.width(), r.height(), fg_color.into());
@ -151,6 +180,10 @@ pub fn loader_indeterminate(
); );
} }
pub fn qrcode(center: Point, data: &str, max_size: u32, case_sensitive: bool) -> Result<(), Error> {
qr::render_qrcode(center.x, center.y, data, max_size, case_sensitive)
}
pub fn text(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) { pub fn text(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text( display::text(
baseline.x, baseline.x,

@ -27,8 +27,7 @@ use crate::ui::event::ButtonEvent;
use crate::ui::event::TouchEvent; use crate::ui::event::TouchEvent;
/// Conversion trait implemented by components that know how to convert their /// Conversion trait implemented by components that know how to convert their
/// message values into MicroPython `Obj`s. We can automatically implement /// message values into MicroPython `Obj`s.
/// `ComponentMsgObj` for components whose message types implement `TryInto`.
pub trait ComponentMsgObj: Component { pub trait ComponentMsgObj: Component {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>; fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error>;
} }

@ -340,28 +340,115 @@ pub struct ButtonStyle {
} }
impl<T> Button<T> { impl<T> Button<T> {
pub fn left_right<F0, F1, R>( pub fn cancel_confirm(
left: Button<T>, left: Button<T>,
left_map: F0,
right: Button<T>, right: Button<T>,
right_map: F1, right_size_factor: usize,
) -> (Map<GridPlaced<Self>, F0>, Map<GridPlaced<Self>, F1>) ) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where where
F0: Fn(ButtonMsg) -> Option<R>,
F1: Fn(ButtonMsg) -> Option<R>,
T: AsRef<str>, T: AsRef<str>,
{ {
let columns = 1 + right_size_factor;
( (
GridPlaced::new(left) GridPlaced::new(left)
.with_grid(1, 3) .with_grid(1, columns)
.with_spacing(theme::BUTTON_SPACING) .with_spacing(theme::BUTTON_SPACING)
.with_row_col(0, 0) .with_row_col(0, 0)
.map(left_map), .map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
}),
GridPlaced::new(right) GridPlaced::new(right)
.with_grid(1, 3) .with_grid(1, columns)
.with_spacing(theme::BUTTON_SPACING) .with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 1), (0, 2)) .with_from_to((0, 1), (0, right_size_factor))
.map(right_map), .map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
) )
} }
pub fn cancel_confirm_text(
left: Option<T>,
right: T,
) -> CancelConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
>
where
T: AsRef<str>,
{
let (left, right_size_factor) = if let Some(verb) = left {
(Button::with_text(verb), 1)
} else {
(Button::with_icon(theme::ICON_CANCEL), 2)
};
let right = Button::with_text(right).styled(theme::button_confirm());
Self::cancel_confirm(left, right, right_size_factor)
}
pub fn cancel_info_confirm(
confirm: T,
info: T,
) -> CancelInfoConfirm<
T,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelInfoConfirmMsg>,
>
where
T: AsRef<str>,
{
let right = Button::with_text(confirm).styled(theme::button_confirm());
let top = Button::with_text(info);
let left = Button::with_icon(theme::ICON_CANCEL);
(
GridPlaced::new(left)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_row_col(1, 0)
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled)
}),
GridPlaced::new(top)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 0), (0, 2))
.map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)),
GridPlaced::new(right)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((1, 1), (1, 2))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed)
}),
)
}
}
type CancelConfirm<T, F0, F1> = (
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
);
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
}
type CancelInfoConfirm<T, F0, F1, F2> = (
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
Map<GridPlaced<Button<T>>, F2>,
);
pub enum CancelInfoConfirmMsg {
Cancelled,
Info,
Confirmed,
} }

@ -1,6 +1,8 @@
use core::ops::Deref;
use crate::ui::{ use crate::ui::{
component::{Child, Component, Event, EventCtx}, component::{Child, Component, Event, EventCtx, Image, Label, Never},
geometry::{Grid, Rect}, geometry::{Grid, Insets, Rect},
}; };
use super::{theme, Button}; use super::{theme, Button};
@ -95,3 +97,94 @@ where
t.close(); t.close();
} }
} }
pub struct IconDialog<T, U> {
image: Child<Image>,
title: Label<T>,
description: Option<Label<T>>,
controls: Child<U>,
}
impl<T, U> IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
{
pub fn new(icon: &'static [u8], title: T, controls: U) -> Self {
Self {
image: Child::new(Image::new(icon)),
title: Label::centered(title, theme::label_warning()),
description: None,
controls: Child::new(controls),
}
}
pub fn with_description(mut self, description: T) -> Self {
self.description = Some(Label::centered(description, theme::label_warning_value()));
self
}
pub const ICON_AREA_HEIGHT: i32 = 64;
pub const DESCRIPTION_SPACE: i32 = 13;
pub const VALUE_SPACE: i32 = 9;
}
impl<T, U> Component for IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
{
type Msg = DialogMsg<Never, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let bounds = bounds.inset(theme::borders());
let (content, buttons) = bounds.split_bottom(Button::<&str>::HEIGHT);
let (image, content) = content.split_top(Self::ICON_AREA_HEIGHT);
let content = content.inset(Insets::top(Self::DESCRIPTION_SPACE));
let (title, content) = content.split_top(self.title.size().y);
let content = content.inset(Insets::top(Self::VALUE_SPACE));
self.image.place(image);
self.title.place(title);
self.description.place(content);
self.controls.place(buttons);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.description.event(ctx, event);
self.controls.event(ctx, event).map(Self::Msg::Controls)
}
fn paint(&mut self) {
self.image.paint();
self.title.paint();
self.description.paint();
self.controls.paint();
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.image.bounds(sink);
self.title.bounds(sink);
self.description.bounds(sink);
self.controls.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for IconDialog<T, U>
where
T: Deref<Target = str>,
U: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("IconDialog");
t.field("title", &self.title);
if let Some(ref description) = self.description {
t.field("description", description);
}
t.field("controls", &self.controls);
t.close();
}
}

@ -7,6 +7,7 @@ use crate::ui::{
pub struct Frame<T, U> { pub struct Frame<T, U> {
area: Rect, area: Rect,
border: Insets,
title: U, title: U,
content: Child<T>, content: Child<T>,
} }
@ -20,10 +21,16 @@ where
Self { Self {
title, title,
area: Rect::zero(), area: Rect::zero(),
border: theme::borders_scroll(),
content: Child::new(content), content: Child::new(content),
} }
} }
pub fn with_border(mut self, border: Insets) -> Self {
self.border = border;
self
}
pub fn inner(&self) -> &T { pub fn inner(&self) -> &T {
self.content.inner() self.content.inner()
} }
@ -37,11 +44,10 @@ where
type Msg = T::Msg; type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
// Same as PageLayout::BUTTON_SPACE. const TITLE_SPACE: i32 = theme::BUTTON_SPACING;
const TITLE_SPACE: i32 = 6;
let (title_area, content_area) = bounds let (title_area, content_area) = bounds
.inset(theme::borders_scroll()) .inset(self.border)
.split_top(theme::FONT_BOLD.text_height()); .split_top(theme::FONT_BOLD.text_height());
let title_area = title_area.inset(Insets::left(theme::CONTENT_BORDER)); let title_area = title_area.inset(Insets::left(theme::CONTENT_BORDER));
let content_area = content_area.inset(Insets::top(TITLE_SPACE)); let content_area = content_area.inset(Insets::top(TITLE_SPACE));

@ -8,8 +8,11 @@ mod page;
mod scroll; mod scroll;
mod swipe; mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet}; pub use button::{
pub use dialog::{Dialog, DialogLayout, DialogMsg}; Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg,
};
pub use dialog::{Dialog, DialogLayout, DialogMsg, IconDialog};
pub use frame::Frame; pub use frame::Frame;
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use keyboard::{ pub use keyboard::{

@ -8,7 +8,7 @@ use crate::ui::{
use super::{ use super::{
hold_to_confirm::{handle_hold_event, CancelHold, CancelHoldMsg}, hold_to_confirm::{handle_hold_event, CancelHold, CancelHoldMsg},
theme, Button, Loader, ScrollBar, Swipe, SwipeDirection, theme, Button, CancelConfirmMsg, Loader, ScrollBar, Swipe, SwipeDirection,
}; };
pub struct SwipePage<T, U> { pub struct SwipePage<T, U> {
@ -19,6 +19,7 @@ pub struct SwipePage<T, U> {
scrollbar: ScrollBar, scrollbar: ScrollBar,
hint: Label<&'static str>, hint: Label<&'static str>,
fade: Option<i32>, fade: Option<i32>,
button_rows: i32,
} }
impl<T, U> SwipePage<T, U> impl<T, U> SwipePage<T, U>
@ -36,9 +37,15 @@ where
pad: Pad::with_background(background), pad: Pad::with_background(background),
hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()), hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()),
fade: None, fade: None,
button_rows: 1,
} }
} }
pub fn with_button_rows(mut self, rows: usize) -> Self {
self.button_rows = rows as i32;
self
}
fn setup_swipe(&mut self) { fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page(); self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page(); self.swipe.allow_down = self.scrollbar.has_previous_page();
@ -69,7 +76,7 @@ where
type Msg = PageMsg<T::Msg, U::Msg>; type Msg = PageMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let layout = PageLayout::new(bounds); let layout = PageLayout::new(bounds, self.button_rows);
self.pad.place(bounds); self.pad.place(bounds);
self.swipe.place(bounds); self.swipe.place(bounds);
self.hint.place(layout.hint); self.hint.place(layout.hint);
@ -186,15 +193,16 @@ pub struct PageLayout {
} }
impl PageLayout { impl PageLayout {
const BUTTON_SPACE: i32 = 6;
const SCROLLBAR_WIDTH: i32 = 10; const SCROLLBAR_WIDTH: i32 = 10;
const SCROLLBAR_SPACE: i32 = 10; const SCROLLBAR_SPACE: i32 = 10;
const HINT_OFF: i32 = 19; const HINT_OFF: i32 = 19;
pub fn new(area: Rect) -> Self { pub fn new(area: Rect, button_rows: i32) -> Self {
let (content, buttons) = area.split_bottom(Button::<&str>::HEIGHT); let buttons_height = button_rows * Button::<&str>::HEIGHT
+ button_rows.saturating_sub(1) * theme::BUTTON_SPACING;
let (content, buttons) = area.split_bottom(buttons_height);
let (_, hint) = area.split_bottom(Self::HINT_OFF); let (_, hint) = area.split_bottom(Self::HINT_OFF);
let (content, _space) = content.split_bottom(Self::BUTTON_SPACE); let (content, _space) = content.split_bottom(theme::BUTTON_SPACING);
let (buttons, _space) = buttons.split_right(theme::CONTENT_BORDER); let (buttons, _space) = buttons.split_right(theme::CONTENT_BORDER);
let (_space, content) = content.split_left(theme::CONTENT_BORDER); let (_space, content) = content.split_left(theme::CONTENT_BORDER);
let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER); let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER);
@ -236,7 +244,7 @@ where
T: Paginate, T: Paginate,
T: Component, T: Component,
{ {
type Msg = PageMsg<T::Msg, bool>; type Msg = PageMsg<T::Msg, CancelConfirmMsg>;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
self.inner.place(bounds); self.inner.place(bounds);
@ -249,7 +257,7 @@ where
let button_msg = match msg { let button_msg = match msg {
Some(PageMsg::Content(c)) => return Some(PageMsg::Content(c)), Some(PageMsg::Content(c)) => return Some(PageMsg::Content(c)),
Some(PageMsg::Controls(CancelHoldMsg::Cancelled)) => { Some(PageMsg::Controls(CancelHoldMsg::Cancelled)) => {
return Some(PageMsg::Controls(false)) return Some(PageMsg::Controls(CancelConfirmMsg::Cancelled))
} }
Some(PageMsg::Controls(CancelHoldMsg::HoldButton(b))) => Some(b), Some(PageMsg::Controls(CancelHoldMsg::HoldButton(b))) => Some(b),
_ => None, _ => None,
@ -262,7 +270,7 @@ where
&mut self.inner.pad, &mut self.inner.pad,
&mut self.inner.content, &mut self.inner.content,
) { ) {
return Some(PageMsg::Controls(true)); return Some(PageMsg::Controls(CancelConfirmMsg::Confirmed));
} }
None None
} }

@ -2,14 +2,25 @@ use core::{convert::TryInto, ops::Deref};
use crate::{ use crate::{
error::Error, error::Error,
micropython::{buffer::StrBuffer, map::Map, module::Module, obj::Obj, qstr::Qstr, util}, micropython::{
buffer::StrBuffer,
iter::{Iter, IterBuf},
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
},
ui::{ ui::{
component::{ component::{
self,
base::ComponentExt, base::ComponentExt,
paginated::{PageMsg, Paginate}, paginated::{PageMsg, Paginate},
painter,
text::paragraphs::Paragraphs, text::paragraphs::Paragraphs,
Component, Component,
}, },
geometry,
layout::{ layout::{
obj::{ComponentMsgObj, LayoutObj}, obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO}, result::{CANCELLED, CONFIRMED, INFO},
@ -19,23 +30,61 @@ use crate::{
use super::{ use super::{
component::{ component::{
Bip39Input, Button, ButtonMsg, Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, Bip39Input, Button, ButtonMsg, CancelConfirmMsg, CancelInfoConfirmMsg, Dialog, DialogMsg,
MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput, MnemonicKeyboard,
PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Slip39Input, SwipeHoldPage, SwipePage, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard,
PinKeyboardMsg, Slip39Input, SwipeHoldPage, SwipePage,
}, },
theme, theme,
}; };
impl TryFrom<CancelConfirmMsg> for Obj {
type Error = Error;
fn try_from(value: CancelConfirmMsg) -> Result<Self, Self::Error> {
match value {
CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}
impl TryFrom<CancelInfoConfirmMsg> for Obj {
type Error = Error;
fn try_from(value: CancelInfoConfirmMsg) -> Result<Self, Self::Error> {
match value {
CancelInfoConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
CancelInfoConfirmMsg::Info => Ok(INFO.as_obj()),
CancelInfoConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}
impl<T, U> ComponentMsgObj for Dialog<T, U> impl<T, U> ComponentMsgObj for Dialog<T, U>
where where
T: ComponentMsgObj, T: ComponentMsgObj,
U: Component<Msg = bool>, U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{ {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
DialogMsg::Content(c) => Ok(self.inner().msg_try_into_obj(c)?), DialogMsg::Content(c) => Ok(self.inner().msg_try_into_obj(c)?),
DialogMsg::Controls(false) => Ok(CANCELLED.as_obj()), DialogMsg::Controls(msg) => msg.try_into(),
DialogMsg::Controls(true) => Ok(CONFIRMED.as_obj()), }
}
}
impl<T, U> ComponentMsgObj for IconDialog<T, U>
where
T: Deref<Target = str>,
U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DialogMsg::Controls(msg) => msg.try_into(),
_ => unreachable!(),
} }
} }
} }
@ -105,13 +154,13 @@ where
impl<T, U> ComponentMsgObj for SwipePage<T, U> impl<T, U> ComponentMsgObj for SwipePage<T, U>
where where
T: Component + Paginate, T: Component + Paginate,
U: Component<Msg = bool>, U: Component,
<U as Component>::Msg: TryInto<Obj, Error = Error>,
{ {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
PageMsg::Content(_) => Err(Error::TypeError), PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()), PageMsg::Controls(msg) => msg.try_into(),
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
} }
} }
} }
@ -123,20 +172,33 @@ where
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
PageMsg::Content(_) => Err(Error::TypeError), PageMsg::Content(_) => Err(Error::TypeError),
PageMsg::Controls(true) => Ok(CONFIRMED.as_obj()), PageMsg::Controls(msg) => msg.try_into(),
PageMsg::Controls(false) => Ok(CANCELLED.as_obj()),
} }
} }
} }
impl<F> ComponentMsgObj for painter::Painter<F>
where
F: FnMut(geometry::Rect),
{
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 { extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let action: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?; let action: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
let description: Option<StrBuffer> = let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let verb: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?; let verb: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_verb, "CONFIRM".into())?;
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?; let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let paragraphs = { let paragraphs = {
let action = action.unwrap_or_default(); let action = action.unwrap_or_default();
@ -154,12 +216,99 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
paragraphs paragraphs
}; };
let buttons = Button::left_right( let obj = if hold {
LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?
} else {
let buttons = Button::cancel_confirm_text(verb_cancel, verb);
LayoutObj::new(
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?
};
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_blob(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 data: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?;
let description: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let extra: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_extra, StrBuffer::empty())?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let _ask_pagination: bool = kwargs.get_or(Qstr::MP_QSTR_ask_pagination, false)?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_BOLD, extra)
.add::<theme::TTDefaultText>(theme::FONT_MONO, data);
let obj = if hold {
LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?
} else {
let buttons = Button::cancel_confirm_text(verb_cancel, "CONFIRM".into());
LayoutObj::new(
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?
};
Ok(obj.into())
};
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()),
1,
);
let obj = LayoutObj::new(
Frame::new(
title,
Dialog::new(
painter::qrcode_painter(address, theme::QR_SIDE_MAX, case_sensitive),
buttons,
),
)
.with_border(theme::borders())
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_output(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: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?;
let verb = "NEXT";
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_MONO, value);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL), Button::with_icon(theme::ICON_CANCEL),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false), Button::with_text(verb).styled(theme::button_confirm()),
Button::with_text(verb.unwrap_or_else(|| "CONFIRM".into())) 2,
.styled(theme::button_confirm()),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
); );
let obj = LayoutObj::new( let obj = LayoutObj::new(
@ -170,15 +319,226 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
extern "C" fn new_confirm_total(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: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
.add::<theme::TTDefaultText>(theme::FONT_MONO, value);
let obj = LayoutObj::new(
Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)).into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_joint_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let spending_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_spending_amount)?.try_into()?;
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "You are contributing:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, spending_amount)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "To the total amount:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, total_amount);
let obj = LayoutObj::new(
Frame::new(
"JOINT TRANSACTION",
SwipeHoldPage::new(paragraphs, theme::BG),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_modify_output(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 sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?;
let amount_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_change)?.try_into()?;
let amount_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_new)?.try_into()?;
let description = if sign < 0 {
"Decrease amount by:"
} else {
"Increase amount by:"
};
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Address:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, address)
// FIXME pagebreak
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description.into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, amount_change)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "New amount:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, amount_new);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text("NEXT").styled(theme::button_confirm()),
2,
);
let obj = LayoutObj::new(
Frame::new(
"MODIFY AMOUNT",
SwipePage::new(paragraphs, buttons, theme::BG),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?;
let user_fee_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_user_fee_change)?.try_into()?;
let total_fee_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_fee_new)?.try_into()?;
let (description, change) = match sign {
s if s < 0 => ("Decrease your fee by:", user_fee_change),
s if s > 0 => ("Increase your fee by:", user_fee_change),
_ => ("Your fee did not change.", StrBuffer::empty()),
};
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description.into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, change)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "\nTransaction fee:".into())
.add::<theme::TTDefaultText>(theme::FONT_MONO, total_fee_new);
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text("NEXT").styled(theme::button_confirm()),
2,
);
let obj = LayoutObj::new(
Frame::new("MODIFY FEE", SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_warning(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: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let buttons = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel()),
Button::with_text("CONTINUE").styled(theme::button_reset()),
2,
);
let obj = LayoutObj::new(
IconDialog::new(theme::IMAGE_WARN, title, buttons).with_description(description),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_success(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: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let buttons = component::Map::new(
Button::with_text(button).styled(theme::button_confirm()),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed),
);
let obj = LayoutObj::new(
IconDialog::new(theme::IMAGE_SUCCESS, title, buttons).with_description(description),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_payment_request(
n_args: usize,
args: *const Obj,
kwargs: *mut Map,
) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let memos: Obj = kwargs.get(Qstr::MP_QSTR_memos)?;
let mut paragraphs =
Paragraphs::new().add::<theme::TTDefaultText>(theme::FONT_NORMAL, description);
let mut iter_buf = IterBuf::new();
let iter = Iter::try_from_obj_with_buf(memos, &mut iter_buf)?;
for memo in iter {
let text: StrBuffer = memo.try_into()?;
paragraphs = paragraphs.add::<theme::TTDefaultText>(theme::FONT_NORMAL, text);
}
let buttons = Button::cancel_info_confirm("CONFIRM", "DETAILS");
let obj = LayoutObj::new(
Frame::new(
"SENDING",
SwipePage::new(paragraphs, buttons, theme::BG).with_button_rows(2),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let coin_name: StrBuffer = kwargs.get(Qstr::MP_QSTR_coin_name)?.try_into()?;
let max_rounds: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_rounds)?.try_into()?;
let max_feerate: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_feerate)?.try_into()?;
let paragraphs = Paragraphs::new()
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Coin name:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, coin_name)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Maximum rounds:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, max_rounds)
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, "Maximum mining fee:".into())
.add::<theme::TTDefaultText>(theme::FONT_BOLD, max_feerate);
let obj = LayoutObj::new(
Frame::new(
"AUTHORIZE COINJOIN",
SwipeHoldPage::new(paragraphs, theme::BG),
)
.into_child(),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let allow_cancel: Option<bool> = let allow_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_allow_cancel, true)?;
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
let warning: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_warning)?.try_into_option()?; let warning: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_warning)?.try_into_option()?;
let obj = LayoutObj::new( let obj = LayoutObj::new(
PinKeyboard::new(prompt, subprompt, warning, allow_cancel.unwrap_or(true)).into_child(), PinKeyboard::new(prompt, subprompt, warning, allow_cancel).into_child(),
)?; )?;
Ok(obj.into()) Ok(obj.into())
}; };
@ -233,16 +593,119 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str | None = None, /// description: str | None = None,
/// verb: str | None = None, /// verb: str | None = None,
/// verb_cancel: str | None = None, /// verb_cancel: str | None = None,
/// hold: bool | None = None, /// hold: bool = False,
/// reverse: bool = False, /// reverse: bool = False,
/// ) -> object: /// ) -> object:
/// """Confirm action.""" /// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
/// def confirm_blob(
/// *,
/// title: str,
/// data: str,
/// description: str = "",
/// extra: str = "",
/// verb_cancel: str | None = None,
/// ask_pagination: bool = False,
/// hold: bool = False,
/// ) -> object:
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).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 confirm_output(
/// *,
/// title: str,
/// description: str,
/// value: str,
/// verb: str = "NEXT",
/// ) -> object:
/// """Confirm output."""
Qstr::MP_QSTR_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(),
/// def confirm_total(
/// *,
/// title: str,
/// description: str,
/// value: str,
/// ) -> object:
/// """Confirm total."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
/// def confirm_joint_total(
/// *,
/// spending_amount: str,
/// total_amount: str,
/// ) -> object:
/// """Confirm total if there are external inputs."""
Qstr::MP_QSTR_confirm_joint_total => obj_fn_kw!(0, new_confirm_joint_total).as_obj(),
/// def confirm_modify_output(
/// *,
/// address: str,
/// sign: int,
/// amount_change: str,
/// amount_new: str,
/// ) -> object:
/// """Decrease or increase amount for given address."""
Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(),
/// def confirm_modify_fee(
/// *,
/// sign: int,
/// user_fee_change: str,
/// total_fee_new: str,
/// ) -> object:
/// """Decrease or increase transaction fee."""
Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(),
/// def show_warning(
/// *,
/// title: str,
/// description: str = "",
/// ) -> object:
/// """Warning modal."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(),
/// def show_success(
/// *,
/// title: str,
/// button: str,
/// description: str = "",
/// ) -> object:
/// """Success modal."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
/// def confirm_payment_request(
/// *,
/// description: str,
/// memos: Iterable[str],
/// ) -> object:
/// """Confirm payment request."""
Qstr::MP_QSTR_confirm_payment_request => obj_fn_kw!(0, new_confirm_payment_request).as_obj(),
/// def confirm_coinjoin(
/// *,
/// coin_name: str,
/// max_rounds: str,
/// max_feerate: str,
/// ) -> object:
/// """Confirm coinjoin authorization."""
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
/// def request_pin( /// def request_pin(
/// *, /// *,
/// prompt: str, /// prompt: str,
/// subprompt: str | None = None, /// subprompt: str,
/// allow_cancel: bool = True, /// allow_cancel: bool = True,
/// warning: str | None = None, /// warning: str | None = None,
/// ) -> str | object: /// ) -> str | object:
@ -295,12 +758,8 @@ mod tests {
#[test] #[test]
fn trace_example_layout() { fn trace_example_layout() {
let buttons = Button::left_right( let buttons =
Button::with_text("Left"), Button::cancel_confirm(Button::with_text("Left"), Button::with_text("Right"), 1);
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false),
Button::with_text("Right"),
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
);
let mut layout = Dialog::new( let mut layout = Dialog::new(
FormattedText::new::<theme::TTDefaultText>( FormattedText::new::<theme::TTDefaultText>(
"Testing text layout, with some text, and some more text. And {param}", "Testing text layout, with some text, and some more text. And {param}",

@ -39,10 +39,13 @@ pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // greyer
// Commonly used corner radius (i.e. for buttons). // Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2; pub const RADIUS: u8 = 2;
// Full-size QR code.
pub const QR_SIDE_MAX: u32 = 140;
// Size of icons in the UI (i.e. inside buttons). // Size of icons in the UI (i.e. inside buttons).
pub const ICON_SIZE: i32 = 16; pub const ICON_SIZE: i32 = 16;
// UI icons. // UI icons (greyscale).
pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif"); pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.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_SPACE: &[u8] = include_res!("model_tt/res/space.toif");
@ -50,6 +53,11 @@ pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif");
pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif"); pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif");
pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif"); pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif");
// Large, color icons.
pub const IMAGE_WARN: &[u8] = include_res!("model_tt/res/warn.toif");
pub const IMAGE_SUCCESS: &[u8] = include_res!("model_tt/res/success.toif");
pub const IMAGE_ERROR: &[u8] = include_res!("model_tt/res/error.toif");
// Scrollbar/PIN dots. // Scrollbar/PIN dots.
pub const DOT_ACTIVE: &[u8] = include_res!("model_tt/res/scroll-active.toif"); pub const DOT_ACTIVE: &[u8] = include_res!("model_tt/res/scroll-active.toif");
pub const DOT_INACTIVE: &[u8] = include_res!("model_tt/res/scroll-inactive.toif"); pub const DOT_INACTIVE: &[u8] = include_res!("model_tt/res/scroll-inactive.toif");
@ -95,6 +103,22 @@ pub fn label_page_hint() -> LabelStyle {
} }
} }
pub fn label_warning() -> LabelStyle {
LabelStyle {
font: FONT_MEDIUM,
text_color: FG,
background_color: BG,
}
}
pub fn label_warning_value() -> LabelStyle {
LabelStyle {
font: FONT_NORMAL,
text_color: OFF_WHITE,
background_color: BG,
}
}
pub fn button_default() -> ButtonStyleSheet { pub fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet { ButtonStyleSheet {
normal: &ButtonStyle { normal: &ButtonStyle {

@ -64,17 +64,131 @@ def confirm_action(
description: str | None = None, description: str | None = None,
verb: str | None = None, verb: str | None = None,
verb_cancel: str | None = None, verb_cancel: str | None = None,
hold: bool | None = None, hold: bool = False,
reverse: bool = False, reverse: bool = False,
) -> object: ) -> object:
"""Confirm action.""" """Confirm action."""
# rust/src/ui/model_tt/layout.rs
def confirm_blob(
*,
title: str,
data: str,
description: str = "",
extra: str = "",
verb_cancel: str | None = None,
ask_pagination: bool = False,
hold: bool = False,
) -> object:
"""Confirm byte sequence data."""
# 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 confirm_output(
*,
title: str,
description: str,
value: str,
verb: str = "NEXT",
) -> object:
"""Confirm output."""
# rust/src/ui/model_tt/layout.rs
def confirm_total(
*,
title: str,
description: str,
value: str,
) -> object:
"""Confirm total."""
# rust/src/ui/model_tt/layout.rs
def confirm_joint_total(
*,
spending_amount: str,
total_amount: str,
) -> object:
"""Confirm total if there are external inputs."""
# rust/src/ui/model_tt/layout.rs
def confirm_modify_output(
*,
address: str,
sign: int,
amount_change: str,
amount_new: str,
) -> object:
"""Decrease or increase amount for given address."""
# rust/src/ui/model_tt/layout.rs
def confirm_modify_fee(
*,
sign: int,
user_fee_change: str,
total_fee_new: str,
) -> object:
"""Decrease or increase transaction fee."""
# rust/src/ui/model_tt/layout.rs
def show_warning(
*,
title: str,
description: str = "",
) -> object:
"""Warning modal."""
# rust/src/ui/model_tt/layout.rs
def show_success(
*,
title: str,
button: str,
description: str = "",
) -> object:
"""Success modal."""
# rust/src/ui/model_tt/layout.rs
def confirm_payment_request(
*,
description: str,
memos: Iterable[str],
) -> object:
"""Confirm payment request."""
# rust/src/ui/model_tt/layout.rs
def confirm_coinjoin(
*,
coin_name: str,
max_rounds: str,
max_feerate: str,
) -> object:
"""Confirm coinjoin authorization."""
# rust/src/ui/model_tt/layout.rs # rust/src/ui/model_tt/layout.rs
def request_pin( def request_pin(
*, *,
prompt: str, prompt: str,
subprompt: str | None = None, subprompt: str,
allow_cancel: bool = True, allow_cancel: bool = True,
warning: str | None = None, warning: str | None = None,
) -> str | object: ) -> str | object:

@ -1,4 +1,5 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import io, log, loop, ui, wire, workflow from trezor import io, log, loop, ui, wire, workflow
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
@ -120,20 +121,16 @@ async def confirm_action(
) -> None: ) -> None:
if isinstance(verb, bytes) or isinstance(verb_cancel, bytes): if isinstance(verb, bytes) or isinstance(verb_cancel, bytes):
raise NotImplementedError raise NotImplementedError
elif isinstance(verb, str): if isinstance(verb, str):
verb = verb.upper() verb = verb.upper()
if isinstance(verb_cancel, str):
verb_cancel = verb_cancel.upper()
if description is not None and description_param is not None: if description is not None and description_param is not None:
if description_param_font != ui.BOLD: if description_param_font != ui.BOLD:
log.error(__name__, "confirm_action description_param_font not implemented") log.error(__name__, "confirm_action description_param_font not implemented")
description = description.format(description_param) description = description.format(description_param)
if hold:
log.error(__name__, "confirm_action hold not implemented")
if verb_cancel:
log.error(__name__, "confirm_action verb_cancel not implemented")
result = await interact( result = await interact(
ctx, ctx,
_RustLayout( _RustLayout(
@ -142,6 +139,7 @@ async def confirm_action(
action=action, action=action,
description=description, description=description,
verb=verb, verb=verb,
verb_cancel=verb_cancel,
hold=hold, hold=hold,
reverse=reverse, reverse=reverse,
) )
@ -172,21 +170,51 @@ async def confirm_backup(ctx: wire.GenericContext) -> bool:
async def confirm_path_warning( async def confirm_path_warning(
ctx: wire.GenericContext, path: str, path_type: str = "Path" ctx: wire.GenericContext, path: str, path_type: str = "Path"
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.show_warning(
title="Unknown path",
description=path,
)
),
"path_warning",
ButtonRequestType.UnknownDerivationPath,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout:
content = _RustLayout(
trezorui2.confirm_blob(
title=title,
data=xpub,
verb_cancel=cancel,
)
)
return content
async def show_xpub( async def show_xpub(
ctx: wire.GenericContext, xpub: str, title: str, cancel: str ctx: wire.GenericContext, xpub: str, title: str, cancel: str
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_show_xpub(xpub, title, cancel),
"show_xpub",
ButtonRequestType.PublicKey,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def show_address( async def show_address(
ctx: wire.GenericContext, ctx: wire.GenericContext,
address: str, address: str,
*, *,
case_sensitive: bool = True,
address_qr: str | None = None, address_qr: str | None = None,
case_sensitive: bool = True,
title: str = "Confirm address", title: str = "Confirm address",
network: str | None = None, network: str | None = None,
multisig_index: int | None = None, multisig_index: int | None = None,
@ -194,7 +222,54 @@ async def show_address(
address_extra: str | None = None, address_extra: str | None = None,
title_qr: str | None = None, title_qr: str | None = None,
) -> None: ) -> None:
raise NotImplementedError is_multisig = len(xpubs) > 0
while True:
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
data=address,
description=network or "",
extra=address_extra or "",
verb_cancel="QR",
)
),
"show_address",
ButtonRequestType.Address,
)
if result is trezorui2.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 trezorui2.CONFIRMED:
break
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 trezorui2.CONFIRMED:
return
def show_pubkey( def show_pubkey(
@ -277,25 +352,27 @@ def show_warning(
) )
def show_success( async def show_success(
ctx: wire.GenericContext, ctx: wire.GenericContext,
br_type: str, br_type: str,
content: str, content: str,
subheader: str | None = None, subheader: str | None = None,
button: str = "Continue", button: str = "Continue",
) -> Awaitable[None]: ) -> None:
return _show_modal( result = await interact(
ctx, ctx,
br_type=br_type, _RustLayout(
br_code=ButtonRequestType.Success, trezorui2.show_success(
header="Success", title=content,
subheader=subheader, description=subheader or "",
content=content, button=button.upper(),
button_confirm=button, )
button_cancel=None, ),
icon=ui.ICON_CONFIRM, br_type,
icon_color=ui.GREEN, ButtonRequestType.Success,
) )
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_output( async def confirm_output(
@ -303,7 +380,7 @@ async def confirm_output(
address: str, address: str,
amount: str, amount: str,
font_amount: int = ui.NORMAL, # TODO cleanup @ redesign font_amount: int = ui.NORMAL, # TODO cleanup @ redesign
title: str = "Confirm sending", title: str = "SENDING",
subtitle: str | None = None, # TODO cleanup @ redesign subtitle: str | None = None, # TODO cleanup @ redesign
color_to: int = ui.FG, # TODO cleanup @ redesign color_to: int = ui.FG, # TODO cleanup @ redesign
to_str: str = " to\n", # TODO cleanup @ redesign to_str: str = " to\n", # TODO cleanup @ redesign
@ -313,7 +390,39 @@ async def confirm_output(
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
icon: str = ui.ICON_SEND, icon: str = ui.ICON_SEND,
) -> None: ) -> None:
raise NotImplementedError title = title.upper()
if title.startswith("CONFIRM "):
title = title[len("CONFIRM ") :]
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title,
description="To:",
value=address,
)
),
"confirm_output",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title,
description="Amount:",
value=amount,
)
),
"confirm_output",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_payment_request( async def confirm_payment_request(
@ -322,7 +431,25 @@ async def confirm_payment_request(
amount: str, amount: str,
memos: list[str], memos: list[str],
) -> Any: ) -> Any:
raise NotImplementedError from ...components.common import confirm
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_payment_request(
description=f"{amount} to\n{recipient_name}",
memos=memos,
)
),
"confirm_payment_request",
ButtonRequestType.ConfirmOutput,
)
if result is trezorui2.CONFIRMED:
return confirm.CONFIRMED
elif result is trezorui2.INFO:
return confirm.INFO
else:
raise wire.ActionCancelled
async def should_show_more( async def should_show_more(
@ -350,7 +477,25 @@ async def confirm_blob(
icon_color: int = ui.GREEN, # TODO cleanup @ redesign icon_color: int = ui.GREEN, # TODO cleanup @ redesign
ask_pagination: bool = False, ask_pagination: bool = False,
) -> None: ) -> None:
raise NotImplementedError if isinstance(data, bytes):
data = hexlify(data).decode()
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
description=description or "",
data=data,
ask_pagination=ask_pagination,
hold=hold,
)
),
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
def confirm_address( def confirm_address(
@ -410,20 +555,60 @@ async def confirm_total(
total_amount: str, total_amount: str,
fee_amount: str, fee_amount: str,
fee_rate_amount: str | None = None, fee_rate_amount: str | None = None,
title: str = "Confirm transaction", title: str = "SENDING",
total_label: str = "Total amount:\n", total_label: str = "Total amount:\n",
fee_label: str = "\nincluding fee:\n", fee_label: str = "\nincluding fee:\n",
icon_color: int = ui.GREEN, icon_color: int = ui.GREEN,
br_type: str = "confirm_total", br_type: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx, br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_output(
title=title.upper(),
description="Fee:",
value=fee_amount,
)
),
"confirm_total",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_total(
title=title.upper(),
description="Total amount:",
value=total_amount,
)
),
"confirm_total",
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_joint_total( async def confirm_joint_total(
ctx: wire.GenericContext, spending_amount: str, total_amount: str ctx: wire.GenericContext, spending_amount: str, total_amount: str
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_joint_total(
spending_amount=spending_amount,
total_amount=total_amount,
)
),
"confirm_joint_total",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_metadata( async def confirm_metadata(
@ -440,13 +625,59 @@ async def confirm_metadata(
icon_color: int = ui.GREEN, # TODO cleanup @ redesign icon_color: int = ui.GREEN, # TODO cleanup @ redesign
larger_vspace: bool = False, # TODO cleanup @ redesign larger_vspace: bool = False, # TODO cleanup @ redesign
) -> None: ) -> None:
raise NotImplementedError if param:
content = content.format(param)
if br_type == "fee_over_threshold":
layout = trezorui2.show_warning(
title="Unusually high fee",
description=param or "",
)
elif br_type == "change_count_over_threshold":
layout = trezorui2.show_warning(
title="A lot of change-outputs",
description=f"{param} outputs" if param is not None else "",
)
else:
if param is not None:
content = content.format(param)
# TODO: "unverified external inputs"
layout = trezorui2.confirm_action(
title=title.upper(),
verb="NEXT",
description=content,
hold=hold,
)
result = await interact(
ctx,
_RustLayout(layout),
br_type,
br_code,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_replacement( async def confirm_replacement(
ctx: wire.GenericContext, description: str, txid: str ctx: wire.GenericContext, description: str, txid: str
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=description.upper(),
description="Confirm transaction ID:",
data=txid,
)
),
"confirm_replacement",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_modify_output( async def confirm_modify_output(
@ -456,7 +687,21 @@ async def confirm_modify_output(
amount_change: str, amount_change: str,
amount_new: str, amount_new: str,
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_modify_output(
address=address,
sign=sign,
amount_change=amount_change,
amount_new=amount_new,
)
),
"modify_output",
ButtonRequestType.ConfirmOutput,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_modify_fee( async def confirm_modify_fee(
@ -465,13 +710,39 @@ async def confirm_modify_fee(
user_fee_change: str, user_fee_change: str,
total_fee_new: str, total_fee_new: str,
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_modify_fee(
sign=sign,
user_fee_change=user_fee_change,
total_fee_new=total_fee_new,
)
),
"modify_fee",
ButtonRequestType.SignTx,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def confirm_coinjoin( async def confirm_coinjoin(
ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str ctx: wire.GenericContext, coin_name: str, max_rounds: int, max_fee_per_vbyte: str
) -> None: ) -> None:
raise NotImplementedError result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_coinjoin(
coin_name=coin_name,
max_rounds=str(max_rounds),
max_feerate=f"{max_fee_per_vbyte} sats/vbyte",
)
),
"coinjoin_final",
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
# TODO cleanup @ redesign # TODO cleanup @ redesign
@ -484,7 +755,42 @@ async def confirm_sign_identity(
async def confirm_signverify( async def confirm_signverify(
ctx: wire.GenericContext, coin: str, message: str, address: str, verify: bool ctx: wire.GenericContext, coin: str, message: str, address: str, verify: bool
) -> None: ) -> None:
raise NotImplementedError if verify:
title = f"VERIFY {coin} MESSAGE"
br_type = "verify_message"
else:
title = f"SIGN {coin} MESSAGE"
br_type = "sign_message"
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title,
description="Confirm address:",
data=address,
)
),
br_type,
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
result = await interact(
ctx,
_RustLayout(
trezorui2.confirm_blob(
title=title,
description="Confirm message:",
data=message,
)
),
br_type,
ButtonRequestType.Other,
)
if result is not trezorui2.CONFIRMED:
raise wire.ActionCancelled
async def show_popup( async def show_popup(

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save