1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-01 12:22:34 +00:00

WIP - design of Receive flow

This commit is contained in:
grdddj 2023-01-02 17:31:37 +01:00
parent c0e657e0d1
commit 47f7ec6972
19 changed files with 307 additions and 115 deletions

View File

@ -60,6 +60,7 @@ static void _librust_qstrs(void) {
MP_QSTR_show_remaining_shares;
MP_QSTR_show_share_words;
MP_QSTR_show_progress;
MP_QSTR_show_receive_address;
MP_QSTR_progress_event;
MP_QSTR_usb_event;
@ -128,4 +129,7 @@ static void _librust_qstrs(void) {
MP_QSTR_fee_label;
MP_QSTR_address_title;
MP_QSTR_amount_title;
MP_QSTR_show_receive_address;
MP_QSTR_derivation_path;
MP_QSTR_address_qr;
}

View File

@ -755,6 +755,16 @@ impl ButtonActions {
)
}
/// Going to last page with left, to the next page with right and confirm
/// with middle
pub fn last_confirm_next() -> Self {
Self::new(
Some(ButtonAction::GoToIndex(-1)),
Some(ButtonAction::Confirm),
Some(ButtonAction::NextPage),
)
}
/// Cancelling with left, going to the next page with right
pub fn cancel_next() -> Self {
Self::new(
@ -769,6 +779,11 @@ impl ButtonActions {
Self::new(None, None, Some(ButtonAction::NextPage))
}
/// Only going to the prev page with left
pub fn only_prev() -> Self {
Self::new(Some(ButtonAction::PrevPage), None, None)
}
/// Cancelling with left, confirming with right
pub fn cancel_confirm() -> Self {
Self::new(

View File

@ -234,12 +234,16 @@ where
fn paint(&mut self) {
self.pad.paint();
// Scrollbars are painted only with a title
if let Some(title) = &self.common_title {
if let Some(title) = self.common_title {
self.scrollbar.paint();
common::paint_header_left(title, self.title_area);
}
self.current_page.paint();
self.buttons.paint();
// On purpose painting current page at the end, after buttons,
// because we sometimes (in the case of QR code) need to use the
// whole height of the display for showing the content
// (and painting buttons last would cover the lower part).
self.current_page.paint();
}
}

View File

@ -3,7 +3,7 @@ use crate::{
ui::{
component::Paginate,
display::{Font, Icon, IconAndName},
geometry::{Alignment, Offset, Rect},
geometry::{Alignment, Offset, Point, Rect},
model_tr::theme,
util::ResultExt,
},
@ -13,7 +13,8 @@ use heapless::Vec;
use super::{
flow_pages_poc_helpers::{
LayoutFit, LayoutSink, Op, TextLayout, TextNoOp, TextRenderer, TextStyle, ToDisplay,
LayoutFit, LayoutSink, Op, QrCodeInfo, TextLayout, TextNoOp, TextRenderer, TextStyle,
ToDisplay,
},
ButtonActions, ButtonDetails, ButtonLayout,
};
@ -148,13 +149,15 @@ impl<const M: usize> Page<M> {
current.btn_left
};
let btn_right = if self.has_next_page() {
Some(ButtonDetails::down_arrow_icon_wide())
// Middle button should be shown only on the last page, not to collide
// with the fat right button.
let (btn_middle, btn_right) = if self.has_next_page() {
(None, Some(ButtonDetails::down_arrow_icon_wide()))
} else {
current.btn_right
(current.btn_middle, current.btn_right)
};
ButtonLayout::new(btn_left, current.btn_middle, btn_right)
ButtonLayout::new(btn_left, btn_middle, btn_right)
}
pub fn btn_actions(&self) -> ButtonActions {
@ -211,6 +214,21 @@ impl<const M: usize> Page<M> {
self.with_new_item(Op::Icon(Icon::new(icon)))
}
pub fn qr_code(
self,
text: StrBuffer,
max_size: i16,
case_sensitive: bool,
center: Point,
) -> Self {
self.with_new_item(Op::QrCode(QrCodeInfo::new(
text,
max_size,
case_sensitive,
center,
)))
}
pub fn font(self, font: Font) -> Self {
self.with_new_item(Op::Font(font))
}

View File

@ -12,7 +12,14 @@ use crate::{
use heapless::Vec;
// TODO: document this
/// To account for operations that are not made of characters
/// but need to be accounted for somehow.
/// Number of processed characters will be increased by this
/// to account for the operation.
const PROCESSED_CHARS_ONE: usize = 1;
/// Container for text allowing for its displaying by chunks
/// without the need to allocate a new String each time.
#[derive(Clone)]
pub struct ToDisplay {
pub text: StrBuffer,
@ -28,15 +35,26 @@ impl ToDisplay {
}
}
/// Holding information about a QR code to be displayed.
#[derive(Clone)]
pub struct QrCodeInfo {
pub text: StrBuffer,
pub data: StrBuffer,
pub max_size: i16,
pub case_sensitive: bool,
pub center: Point,
}
// TODO: add QrCode(QrCodeInfo)
impl QrCodeInfo {
pub fn new(data: StrBuffer, max_size: i16, case_sensitive: bool, center: Point) -> Self {
Self {
data,
max_size,
case_sensitive,
center,
}
}
}
/// Operations that can be done on the screen.
#[derive(Clone)]
pub enum Op {
@ -44,6 +62,8 @@ pub enum Op {
Text(ToDisplay),
/// Render icon.
Icon(Icon),
/// Render QR Code.
QrCode(QrCodeInfo),
/// Set current text color.
Color(Color),
/// Set currently used font.
@ -218,13 +238,15 @@ impl TextLayout {
}
}
Op::Icon(_) if skipped < skip_bytes => {
// Assume the icon accounts for one character
skipped = skipped.saturating_add(1);
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
}
Op::QrCode(_) if skipped < skip_bytes => {
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
}
Op::NextPage if skipped < skip_bytes => {
// Skip the next page and consider it one character
skipped = skipped.saturating_add(1);
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
}
Op::CursorOffset(_) if skipped < skip_bytes => {
@ -258,16 +280,28 @@ impl TextLayout {
Op::NextPage => {
// Pretending that nothing more fits on current page to force
// continuing on the next one
// Making that to account for one character for pagination purposes
total_processed_chars += 1;
total_processed_chars += PROCESSED_CHARS_ONE;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
Op::QrCode(qr_details) => {
self.layout_qr_code(qr_details, sink);
// QR codes are always the last component that can be shown
// on the given page (meaning a series of Op's).
// Throwing Fitting to force the end of the whole page.
// (It would be too complicated to account for it by modifying cursor, etc.,
// and there is not a need for it currently. If we want QR code together
// with some other things on the same screen, just first render the other
// things and do the QR code last.)
total_processed_chars += PROCESSED_CHARS_ONE;
return LayoutFit::Fitting {
processed_chars: total_processed_chars,
height: self.layout_height(init_cursor, *cursor),
};
}
// Drawing text or icon
// TODO: add QRCode support - always returning OOB
// to force going to the next page
Op::Text(_) | Op::Icon(_) => {
// Text and Icon behave similarly - we try to fit them
// on the current page and if they do not fit,
@ -452,12 +486,18 @@ impl TextLayout {
cursor.x += icon.width() as i16;
LayoutFit::Fitting {
// TODO: unify the 1 being returned - make it a CONST probably
processed_chars: 1,
processed_chars: PROCESSED_CHARS_ONE,
height: 0, // it should just draw on one line
}
}
/// Fitting the QR code on the current screen.
/// Not returning `LayoutFit`, QR codes are handled differently,
/// they automatically throw out of bounds.
pub fn layout_qr_code(&self, qr_code_info: QrCodeInfo, sink: &mut dyn LayoutSink) {
sink.qrcode(qr_code_info);
}
/// Overall height of the content, including paddings.
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
self.padding_top
@ -495,7 +535,7 @@ pub trait LayoutSink {
/// Icon should be displayed.
fn icon(&mut self, _cursor: Point, _layout: &TextLayout, _icon: Icon) {}
/// QR code should be displayed.
fn qrcode(&mut self, _cursor: Point, _layout: &TextLayout, _qr_code: QrCodeInfo) {}
fn qrcode(&mut self, _qr_code: QrCodeInfo) {}
/// Hyphen at the end of line.
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
/// Ellipsis at the end of the page.
@ -594,6 +634,16 @@ impl LayoutSink for TextRenderer {
layout.style.background_color,
);
}
fn qrcode(&mut self, qr_code: QrCodeInfo) {
display::qrcode(
qr_code.center,
qr_code.data.as_ref(),
qr_code.max_size as _,
qr_code.case_sensitive,
)
.unwrap_or(())
}
}
/// `LayoutSink` for debugging purposes.
@ -613,6 +663,11 @@ impl<'a> LayoutSink for TraceSink<'a> {
icon.trace(self.0);
}
fn qrcode(&mut self, qr_code: QrCodeInfo) {
self.0.string("QR code: ");
self.0.string(qr_code.data.as_ref());
}
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
self.0.string("-");
}

View File

@ -18,7 +18,6 @@ use crate::{
component::{
base::Component,
paginated::{PageMsg, Paginate},
painter,
text::paragraphs::{
Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, Paragraphs, VecExt,
},
@ -41,7 +40,7 @@ use super::{
NoBtnDialogMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress,
QRCodePage, QRCodePageMessage, ShareWords, SimpleChoice, SimpleChoiceMsg,
},
theme,
constant, theme,
};
pub enum CancelConfirmMsg {
@ -373,24 +372,81 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
extern "C" fn new_show_receive_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let verb_cancel: StrBuffer = kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into()?;
let address_qr: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_qr)?.try_into()?;
let account: StrBuffer = kwargs.get(Qstr::MP_QSTR_account)?.try_into()?;
let derivation_path: StrBuffer = kwargs.get(Qstr::MP_QSTR_derivation_path)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let verb: StrBuffer = "CONFIRM".into();
let get_page = move |page_index| {
// Showing two screens - the recipient address and summary confirmation
match page_index {
0 => {
// RECEIVE ADDRESS
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::cancel_icon()),
Some(ButtonDetails::armed_text("CONFIRM".into())),
Some(ButtonDetails::text("i".into())),
);
let btn_actions = ButtonActions::last_confirm_next();
Page::<15>::new(btn_layout, btn_actions, Font::BOLD)
.text_bold(title)
.newline()
.newline_half()
.text_mono(address)
}
1 => {
// QR CODE
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::right_arrow_icon()),
);
let btn_actions = ButtonActions::prev_next();
Page::<15>::new(btn_layout, btn_actions, Font::MONO).qr_code(
address_qr,
theme::QR_SIDE_MAX,
case_sensitive,
constant::screen().center(),
)
}
2 => {
// ADDRESS INFO
let btn_layout =
ButtonLayout::new(Some(ButtonDetails::left_arrow_icon()), None, None);
let btn_actions = ButtonActions::only_prev();
Page::<15>::new(btn_layout, btn_actions, Font::MONO)
.text_bold("Account:".into())
.newline()
.text_mono(account)
.newline()
.text_bold("Derivation path:".into())
.newline()
.text_mono(derivation_path)
}
3 => {
// ADDRESS MISMATCH
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::text("QUIT".into())),
);
let btn_actions = ButtonActions::beginning_cancel();
Page::<15>::new(btn_layout, btn_actions, Font::MONO)
.text_bold("ADDRESS MISMATCH?".into())
.newline()
.newline_half()
.text_mono("Please contact Trezor support on trezor.io/support".into())
}
_ => unreachable!(),
}
};
let pages = FlowPages::new(get_page, 4);
let qr_code = painter::qrcode_painter(address, theme::QR_SIDE_MAX as u32, case_sensitive);
let btn_layout = ButtonLayout::new(
Some(ButtonDetails::text(verb_cancel)),
None,
Some(ButtonDetails::text(verb)),
);
let obj = LayoutObj::new(QRCodePage::new(title, qr_code, btn_layout))?;
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -760,15 +816,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm summary of a transaction. Specific for model R."""
Qstr::MP_QSTR_confirm_total_r => obj_fn_kw!(0, new_confirm_total).as_obj(),
/// def show_qr(
/// def show_receive_address(
/// *,
/// title: str,
/// address: str,
/// verb_cancel: str,
/// address_qr: str,
/// account: str,
/// derivation_path: str,
/// case_sensitive: bool,
/// ) -> object:
/// """Show QR code."""
Qstr::MP_QSTR_show_qr => obj_fn_kw!(0, new_show_qr).as_obj(),
/// """Show receive address together with QR code and details about it."""
Qstr::MP_QSTR_show_receive_address => obj_fn_kw!(0, new_show_receive_address).as_obj(),
/// def show_info(
/// *,

View File

@ -65,5 +65,7 @@ pub const BUTTON_CONTENT_HEIGHT: i16 = 7;
pub const BUTTON_OUTLINE: i16 = 3;
pub const BUTTON_HEIGHT: i16 = BUTTON_CONTENT_HEIGHT + 2 * BUTTON_OUTLINE;
// Full-size QR code.
pub const QR_SIDE_MAX: i16 = 64 - BUTTON_HEIGHT;
/// Full-size QR code.
/// Accounting for little larger QR code than the screen,
/// to fit taproot addresses (top and bottom row will not be visible).
pub const QR_SIDE_MAX: i16 = 66;

View File

@ -60,14 +60,16 @@ def confirm_total_r(
# rust/src/ui/model_tr/layout.rs
def show_qr(
def show_receive_address(
*,
title: str,
address: str,
verb_cancel: str,
address_qr: str,
account: str,
derivation_path: str,
case_sensitive: bool,
) -> object:
"""Show QR code."""
"""Show receive address together with QR code and details about it."""
# rust/src/ui/model_tr/layout.rs

View File

@ -28,7 +28,12 @@ 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)
derivation_path = paths.address_n_to_str(address_n)
await show_address(
ctx,
address,
derivation_path=derivation_path,
account="Binance",
)
return BinanceAddress(address=address)

View File

@ -112,13 +112,38 @@ async def get_address(
xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes),
)
else:
title = address_n_to_str(address_n)
derivation_path = address_n_to_str(address_n)
await show_address(
ctx,
address_short,
address_qr=address,
case_sensitive=address_case_sensitive,
title=title,
derivation_path=derivation_path,
account=_path_to_account(derivation_path, coin.coin_shortcut),
)
return Address(address=address, mac=mac)
def _path_to_account(path: str, coin_shortcut: str) -> str:
"""Transforms a BIP-32 path to a human-readable account name.
Examples: (m/44'/0'/0/0/0', BTC) -> BTC Legacy #1
(m/84'/0'/3/0/0', BTC) -> BTC Segwit #4
"""
path = path.lstrip("m/")
purpose = path.split("/")[0].rstrip("'hH")
try:
account_num = int(path.split("/")[2].rstrip("'hH"))
except (IndexError, ValueError):
account_num = 0
purpose_str = {
"44": "Legacy",
"49": "L.Segwit",
"84": "Segwit",
"86": "Taproot",
}.get(purpose, "Unknown")
return f"{coin_shortcut} {purpose_str} #{account_num + 1}"

View File

@ -924,7 +924,7 @@ async def show_cardano_address(
network_name = protocol_magics.to_ui_string(protocol_magic)
title = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]} address"
address_extra = None
derivation_path = None
title_qr = title
if address_parameters.address_type in (
CAT.BYRON,
@ -935,10 +935,10 @@ async def show_cardano_address(
CAT.REWARD,
):
if address_parameters.address_n:
address_extra = address_n_to_str(address_parameters.address_n)
derivation_path = address_n_to_str(address_parameters.address_n)
title_qr = 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)
derivation_path = address_n_to_str(address_parameters.address_n_staking)
title_qr = address_n_to_str(address_parameters.address_n_staking)
await layouts.show_address(
@ -946,6 +946,8 @@ async def show_cardano_address(
address,
title=title,
network=network_name,
address_extra=address_extra,
address_extra=derivation_path,
title_qr=title_qr,
derivation_path=derivation_path,
account="Cardano",
)

View File

@ -32,7 +32,12 @@ 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)
derivation_path = paths.address_n_to_str(address_n)
await show_address(
ctx,
address,
derivation_path=derivation_path,
account="Ethereum",
)
return EthereumAddress(address=address)

View File

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

View File

@ -30,13 +30,14 @@ async def get_address(
address = node.nem_address(network)
if msg.show_display:
title = address_n_to_str(address_n)
derivation_path = address_n_to_str(address_n)
await show_address(
ctx,
address,
case_sensitive=False,
title=title,
network=get_network_str(network),
derivation_path=derivation_path,
account="NEM",
)
return NEMAddress(address=address)

View File

@ -25,7 +25,12 @@ 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)
derivation_path = paths.address_n_to_str(msg.address_n)
await show_address(
ctx,
address,
derivation_path=derivation_path,
account="Ripple",
)
return RippleAddress(address=address)

View File

@ -24,7 +24,13 @@ 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)
derivation_path = paths.address_n_to_str(msg.address_n)
await show_address(
ctx,
address,
case_sensitive=False,
derivation_path=derivation_path,
account="Stellar",
)
return StellarAddress(address=address)

View File

@ -29,7 +29,12 @@ 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)
derivation_path = paths.address_n_to_str(msg.address_n)
await show_address(
ctx,
address,
derivation_path=derivation_path,
account="Tezos",
)
return TezosAddress(address=address)

View File

@ -655,69 +655,39 @@ async def show_address(
*,
case_sensitive: bool = True,
address_qr: str | None = None,
title: str = "Confirm address",
title: 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,
derivation_path: str | None = None,
account: str | None = None,
) -> None:
is_multisig = len(xpubs) > 0
# TODO: replace with confirm_blob
data = address
if network:
data += f"\n\n{network}"
if address_extra:
data += f"\n\n{address_extra}"
while True:
result = await interact(
account = account or "Unknown"
derivation_path = derivation_path or "Unknown"
title = title or "Receive address"
await raise_if_cancelled(
interact(
ctx,
RustLayout(
trezorui2.confirm_action(
trezorui2.show_receive_address(
title=title.upper(),
action=data,
description=None,
verb="CONFIRM",
verb_cancel="QR",
reverse=False,
hold=False,
address=address,
address_qr=address if address_qr is None else address_qr,
account=account,
derivation_path=derivation_path,
case_sensitive=case_sensitive,
)
),
"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
# TODO: support showing multisig xpubs?
# TODO: send button requests in the flow above?
def show_pubkey(

View File

@ -360,20 +360,29 @@ async def show_address(
*,
address_qr: str | None = None,
case_sensitive: bool = True,
title: str = "Confirm address",
title: 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,
derivation_path: str | None = None,
account: str | None = None,
) -> None:
# TODO: could show the derivation path and account, the same was as TR
is_multisig = len(xpubs) > 0
if title:
title = title.upper()
elif derivation_path:
title = derivation_path.upper()
else:
title = "CONFIRM ADDRESS"
while True:
result = await interact(
ctx,
RustLayout(
trezorui2.confirm_blob(
title=title.upper(),
title=title,
data=address,
description=network or "",
extra=address_extra or "",