mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 15:38:11 +00:00
feat(core): create new Ethereum send flow for TR
[no changelog]
This commit is contained in:
parent
61eb47636a
commit
eed6e0b71c
@ -41,6 +41,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_confirm_blob;
|
||||
MP_QSTR_confirm_coinjoin;
|
||||
MP_QSTR_confirm_emphasized;
|
||||
MP_QSTR_confirm_ethereum_tx;
|
||||
MP_QSTR_confirm_fido;
|
||||
MP_QSTR_confirm_homescreen;
|
||||
MP_QSTR_confirm_joint_total;
|
||||
@ -85,6 +86,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_max_feerate;
|
||||
MP_QSTR_max_len;
|
||||
MP_QSTR_max_rounds;
|
||||
MP_QSTR_maximum_fee;
|
||||
MP_QSTR_min_count;
|
||||
MP_QSTR_multiple_pages_texts;
|
||||
MP_QSTR_notification;
|
||||
@ -95,6 +97,7 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_path;
|
||||
MP_QSTR_progress_event;
|
||||
MP_QSTR_prompt;
|
||||
MP_QSTR_recipient;
|
||||
MP_QSTR_request_bip39;
|
||||
MP_QSTR_request_complete_repaint;
|
||||
MP_QSTR_request_number;
|
||||
|
@ -16,7 +16,7 @@ use heapless::Vec;
|
||||
|
||||
// So that there is only one implementation, and not multiple generic ones
|
||||
// as would be via `const N: usize` generics.
|
||||
const MAX_OPS: usize = 15;
|
||||
const MAX_OPS: usize = 20;
|
||||
|
||||
/// To account for operations that are not made of characters
|
||||
/// but need to be accounted for somehow.
|
||||
@ -39,6 +39,10 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.ops.len() == 0
|
||||
}
|
||||
|
||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.layout.bounds = bounds;
|
||||
bounds
|
||||
|
@ -383,9 +383,9 @@ where
|
||||
pub fn from_text_possible_icon(text: T) -> Self {
|
||||
if text.as_ref() == "" {
|
||||
Self::cancel_icon()
|
||||
} else if text.as_ref() == "left_arrow_icon" {
|
||||
} else if text.as_ref() == "<" {
|
||||
Self::left_arrow_icon()
|
||||
} else if text.as_ref() == "up_arrow_icon" {
|
||||
} else if text.as_ref() == "^" {
|
||||
Self::up_arrow_icon()
|
||||
} else {
|
||||
Self::text(text)
|
||||
@ -662,7 +662,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel cross on left and right arrow facing down.
|
||||
/// Up arrow on left and right arrow facing down.
|
||||
pub fn up_arrow_none_arrow_wide() -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::up_arrow_icon()),
|
||||
@ -671,6 +671,15 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Up arrow on left, middle text and info on the right.
|
||||
pub fn up_arrow_armed_info(text: T) -> Self {
|
||||
Self::new(
|
||||
Some(ButtonDetails::up_arrow_icon()),
|
||||
Some(ButtonDetails::armed_text(text)),
|
||||
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel cross on left and right arrow facing down.
|
||||
pub fn cancel_none_arrow_down() -> Self {
|
||||
Self::new(
|
||||
|
@ -84,6 +84,7 @@ where
|
||||
current_page: usize,
|
||||
page_count: usize,
|
||||
title: Option<T>,
|
||||
slim_arrows: bool,
|
||||
}
|
||||
|
||||
// For `layout.rs`
|
||||
@ -103,6 +104,7 @@ where
|
||||
current_page: 0,
|
||||
page_count: 1,
|
||||
title: None,
|
||||
slim_arrows: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,6 +120,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Using slim arrows instead of wide buttons.
|
||||
pub fn with_slim_arrows(mut self) -> Self {
|
||||
self.slim_arrows = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) {
|
||||
self.change_page(self.current_page);
|
||||
self.formatted.paint();
|
||||
@ -137,17 +145,29 @@ where
|
||||
// On the last page showing only the narrow arrow, so the right
|
||||
// button with possibly long text has enough space.
|
||||
let btn_left = if self.has_prev_page() && !self.has_next_page() {
|
||||
Some(ButtonDetails::up_arrow_icon())
|
||||
if self.slim_arrows {
|
||||
Some(ButtonDetails::left_arrow_icon())
|
||||
} else {
|
||||
Some(ButtonDetails::up_arrow_icon())
|
||||
}
|
||||
} else if self.has_prev_page() {
|
||||
Some(ButtonDetails::up_arrow_icon_wide())
|
||||
if self.slim_arrows {
|
||||
Some(ButtonDetails::left_arrow_icon())
|
||||
} else {
|
||||
Some(ButtonDetails::up_arrow_icon_wide())
|
||||
}
|
||||
} else {
|
||||
current.btn_left
|
||||
};
|
||||
|
||||
// Middle button should be shown only on the last page, not to collide
|
||||
// with the fat right button.
|
||||
// with the possible fat right button.
|
||||
let (btn_middle, btn_right) = if self.has_next_page() {
|
||||
(None, Some(ButtonDetails::down_arrow_icon_wide()))
|
||||
if self.slim_arrows {
|
||||
(None, Some(ButtonDetails::right_arrow_icon()))
|
||||
} else {
|
||||
(None, Some(ButtonDetails::down_arrow_icon_wide()))
|
||||
}
|
||||
} else {
|
||||
(current.btn_middle, current.btn_right)
|
||||
};
|
||||
|
@ -679,6 +679,76 @@ 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_confirm_ethereum_tx(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||
let block = |_args: &[Obj], kwargs: &Map| {
|
||||
let recipient: StrBuffer = kwargs.get(Qstr::MP_QSTR_recipient)?.try_into()?;
|
||||
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
|
||||
let maximum_fee: StrBuffer = kwargs.get(Qstr::MP_QSTR_maximum_fee)?.try_into()?;
|
||||
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
|
||||
|
||||
let get_page = move |page_index| {
|
||||
match page_index {
|
||||
0 => {
|
||||
// RECIPIENT
|
||||
let btn_layout = ButtonLayout::cancel_none_text("CONTINUE".into());
|
||||
let btn_actions = ButtonActions::cancel_none_next();
|
||||
|
||||
let ops = OpTextLayout::new(theme::TEXT_MONO_DATA).text_mono(recipient.clone());
|
||||
|
||||
let formatted = FormattedText::new(ops).vertically_centered();
|
||||
Page::new(btn_layout, btn_actions, formatted).with_title("RECIPIENT".into())
|
||||
}
|
||||
1 => {
|
||||
// Total amount + fee
|
||||
let btn_layout = ButtonLayout::up_arrow_armed_info("CONFIRM".into());
|
||||
let btn_actions = ButtonActions::prev_confirm_next();
|
||||
|
||||
let ops = OpTextLayout::new(theme::TEXT_MONO)
|
||||
.text_mono(total_amount.clone())
|
||||
.newline()
|
||||
.newline_half()
|
||||
.text_bold("Maximum fee:".into())
|
||||
.newline()
|
||||
.text_mono(maximum_fee.clone());
|
||||
|
||||
let formatted = FormattedText::new(ops);
|
||||
Page::new(btn_layout, btn_actions, formatted).with_title("Amount:".into())
|
||||
}
|
||||
2 => {
|
||||
// Fee information
|
||||
let btn_layout = ButtonLayout::arrow_none_none();
|
||||
let btn_actions = ButtonActions::prev_none_none();
|
||||
|
||||
let mut ops = OpTextLayout::new(theme::TEXT_MONO);
|
||||
|
||||
for item in unwrap!(IterBuf::new().try_iterate(items)) {
|
||||
let [key, value]: [Obj; 2] = unwrap!(iter_into_array(item));
|
||||
if !ops.is_empty() {
|
||||
// Each key-value pair is on its own page
|
||||
ops = ops.next_page();
|
||||
}
|
||||
ops = ops
|
||||
.text_bold(unwrap!(key.try_into()))
|
||||
.newline()
|
||||
.text_mono(unwrap!(value.try_into()));
|
||||
}
|
||||
|
||||
let formatted = FormattedText::new(ops).vertically_centered();
|
||||
Page::new(btn_layout, btn_actions, formatted)
|
||||
.with_title("FEE INFORMATION".into())
|
||||
.with_slim_arrows()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
let pages = FlowPages::new(get_page, 3);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
}
|
||||
|
||||
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()?;
|
||||
@ -1099,7 +1169,7 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map
|
||||
title,
|
||||
paragraphs.into_paragraphs(),
|
||||
button,
|
||||
Some("left_arrow_icon".into()),
|
||||
Some("<".into()),
|
||||
false,
|
||||
)
|
||||
};
|
||||
@ -1599,6 +1669,16 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
||||
/// """Confirm summary of a transaction."""
|
||||
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
|
||||
|
||||
/// def confirm_ethereum_tx(
|
||||
/// *,
|
||||
/// recipient: str,
|
||||
/// total_amount: str,
|
||||
/// maximum_fee: str,
|
||||
/// items: Iterable[Tuple[str, str]],
|
||||
/// ) -> object:
|
||||
/// """Confirm details about Ethereum transaction."""
|
||||
Qstr::MP_QSTR_confirm_ethereum_tx => obj_fn_kw!(0, new_confirm_ethereum_tx).as_obj(),
|
||||
|
||||
/// def tutorial() -> object:
|
||||
/// """Show user how to interact with the device."""
|
||||
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
|
||||
|
@ -149,6 +149,17 @@ def confirm_total(
|
||||
"""Confirm summary of a transaction."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tr/layout.rs
|
||||
def confirm_ethereum_tx(
|
||||
*,
|
||||
recipient: str,
|
||||
total_amount: str,
|
||||
maximum_fee: str,
|
||||
items: Iterable[Tuple[str, str]],
|
||||
) -> object:
|
||||
"""Confirm details about Ethereum transaction."""
|
||||
|
||||
|
||||
# rust/src/ui/model_tr/layout.rs
|
||||
def tutorial() -> object:
|
||||
"""Show user how to interact with the device."""
|
||||
|
@ -4,14 +4,13 @@ from trezor import ui
|
||||
from trezor.enums import ButtonRequestType
|
||||
from trezor.strings import format_plural
|
||||
from trezor.ui.layouts import (
|
||||
confirm_amount,
|
||||
confirm_blob,
|
||||
confirm_ethereum_tx,
|
||||
confirm_text,
|
||||
confirm_total,
|
||||
should_show_more,
|
||||
)
|
||||
|
||||
from .helpers import decode_typed_data
|
||||
from .helpers import address_from_bytes, decode_typed_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Awaitable, Iterable
|
||||
@ -24,72 +23,61 @@ if TYPE_CHECKING:
|
||||
)
|
||||
|
||||
|
||||
def require_confirm_tx(
|
||||
async def require_confirm_tx(
|
||||
to_bytes: bytes,
|
||||
value: int,
|
||||
network: EthereumNetworkInfo,
|
||||
token: EthereumTokenInfo | None,
|
||||
) -> Awaitable[None]:
|
||||
from trezor.ui.layouts import confirm_output
|
||||
|
||||
from .helpers import address_from_bytes
|
||||
|
||||
if to_bytes:
|
||||
to_str = address_from_bytes(to_bytes, network)
|
||||
else:
|
||||
to_str = "new contract?"
|
||||
return confirm_output(
|
||||
to_str,
|
||||
format_ethereum_amount(value, token, network),
|
||||
br_code=ButtonRequestType.SignTx,
|
||||
)
|
||||
|
||||
|
||||
async def require_confirm_fee(
|
||||
spending: int,
|
||||
gas_price: int,
|
||||
gas_limit: int,
|
||||
network: EthereumNetworkInfo,
|
||||
token: EthereumTokenInfo | None,
|
||||
) -> None:
|
||||
await confirm_amount(
|
||||
title="Confirm fee",
|
||||
description="Gas price:",
|
||||
amount=format_ethereum_amount(gas_price, None, network),
|
||||
)
|
||||
await confirm_total(
|
||||
total_amount=format_ethereum_amount(spending, token, network),
|
||||
fee_amount=format_ethereum_amount(gas_price * gas_limit, None, network),
|
||||
total_label="Amount sent:",
|
||||
fee_label="Maximum fee:",
|
||||
if to_bytes:
|
||||
to_str = address_from_bytes(to_bytes, network)
|
||||
else:
|
||||
to_str = "new contract?"
|
||||
|
||||
total_amount = format_ethereum_amount(value, token, network)
|
||||
maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, network)
|
||||
gas_limit_str = f"{gas_limit} units"
|
||||
gas_price_str = format_ethereum_amount(gas_price, None, network)
|
||||
|
||||
items = (
|
||||
("Gas limit:", gas_limit_str),
|
||||
("Gas price:", gas_price_str),
|
||||
)
|
||||
|
||||
await confirm_ethereum_tx(to_str, total_amount, maximum_fee, items)
|
||||
|
||||
async def require_confirm_eip1559_fee(
|
||||
spending: int,
|
||||
max_priority_fee: int,
|
||||
|
||||
async def require_confirm_tx_eip1559(
|
||||
to_bytes: bytes,
|
||||
value: int,
|
||||
max_gas_fee: int,
|
||||
max_priority_fee: int,
|
||||
gas_limit: int,
|
||||
network: EthereumNetworkInfo,
|
||||
token: EthereumTokenInfo | None,
|
||||
) -> None:
|
||||
await confirm_amount(
|
||||
"Confirm fee",
|
||||
format_ethereum_amount(max_gas_fee, None, network),
|
||||
"Maximum fee per gas",
|
||||
)
|
||||
await confirm_amount(
|
||||
"Confirm fee",
|
||||
format_ethereum_amount(max_priority_fee, None, network),
|
||||
"Priority fee per gas",
|
||||
)
|
||||
await confirm_total(
|
||||
format_ethereum_amount(spending, token, network),
|
||||
format_ethereum_amount(max_gas_fee * gas_limit, None, network),
|
||||
total_label="Amount sent:",
|
||||
fee_label="Maximum fee:",
|
||||
|
||||
if to_bytes:
|
||||
to_str = address_from_bytes(to_bytes, network)
|
||||
else:
|
||||
to_str = "new contract?"
|
||||
|
||||
total_amount = format_ethereum_amount(value, token, network)
|
||||
maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, network)
|
||||
gas_limit_str = f"{gas_limit} units"
|
||||
max_gas_fee_str = format_ethereum_amount(max_gas_fee, None, network)
|
||||
max_priority_fee_str = format_ethereum_amount(max_priority_fee, None, network)
|
||||
|
||||
items = (
|
||||
("Gas limit:", gas_limit_str),
|
||||
("Max gas price:", max_gas_fee_str),
|
||||
("Priority fee:", max_priority_fee_str),
|
||||
)
|
||||
|
||||
await confirm_ethereum_tx(to_str, total_amount, maximum_fee, items)
|
||||
|
||||
|
||||
def require_confirm_unknown_token(address_bytes: bytes) -> Awaitable[None]:
|
||||
from ubinascii import hexlify
|
||||
|
@ -33,7 +33,7 @@ async def sign_tx(
|
||||
|
||||
from apps.common import paths
|
||||
|
||||
from .layout import require_confirm_data, require_confirm_fee, require_confirm_tx
|
||||
from .layout import require_confirm_data, require_confirm_tx
|
||||
|
||||
# check
|
||||
if msg.tx_type not in [1, 6, None]:
|
||||
@ -47,13 +47,13 @@ async def sign_tx(
|
||||
# Handle ERC20s
|
||||
token, address_bytes, recipient, value = await handle_erc20(msg, defs)
|
||||
|
||||
data_total = msg.data_length
|
||||
data_total = msg.data_length # local_cache_attribute
|
||||
|
||||
await require_confirm_tx(recipient, value, defs.network, token)
|
||||
if token is None and msg.data_length > 0:
|
||||
if token is None and data_total > 0:
|
||||
await require_confirm_data(msg.data_initial_chunk, data_total)
|
||||
|
||||
await require_confirm_fee(
|
||||
await require_confirm_tx(
|
||||
recipient,
|
||||
value,
|
||||
int.from_bytes(msg.gas_price, "big"),
|
||||
int.from_bytes(msg.gas_limit, "big"),
|
||||
|
@ -42,14 +42,11 @@ async def sign_tx_eip1559(
|
||||
|
||||
from apps.common import paths
|
||||
|
||||
from .layout import (
|
||||
require_confirm_data,
|
||||
require_confirm_eip1559_fee,
|
||||
require_confirm_tx,
|
||||
)
|
||||
from .layout import require_confirm_data, require_confirm_tx_eip1559
|
||||
from .sign_tx import check_common_fields, handle_erc20, send_request_chunk
|
||||
|
||||
gas_limit = msg.gas_limit # local_cache_attribute
|
||||
data_total = msg.data_length # local_cache_attribute
|
||||
|
||||
# check
|
||||
if len(msg.max_gas_fee) + len(gas_limit) > 30:
|
||||
@ -63,16 +60,14 @@ async def sign_tx_eip1559(
|
||||
# Handle ERC20s
|
||||
token, address_bytes, recipient, value = await handle_erc20(msg, defs)
|
||||
|
||||
data_total = msg.data_length
|
||||
|
||||
await require_confirm_tx(recipient, value, defs.network, token)
|
||||
if token is None and msg.data_length > 0:
|
||||
if token is None and data_total > 0:
|
||||
await require_confirm_data(msg.data_initial_chunk, data_total)
|
||||
|
||||
await require_confirm_eip1559_fee(
|
||||
await require_confirm_tx_eip1559(
|
||||
recipient,
|
||||
value,
|
||||
int.from_bytes(msg.max_priority_fee, "big"),
|
||||
int.from_bytes(msg.max_gas_fee, "big"),
|
||||
int.from_bytes(msg.max_priority_fee, "big"),
|
||||
int.from_bytes(gas_limit, "big"),
|
||||
defs.network,
|
||||
token,
|
||||
|
@ -972,6 +972,30 @@ async def confirm_total(
|
||||
)
|
||||
|
||||
|
||||
async def confirm_ethereum_tx(
|
||||
recipient: str,
|
||||
total_amount: str,
|
||||
maximum_fee: str,
|
||||
items: Iterable[tuple[str, str]],
|
||||
br_type: str = "confirm_ethereum_tx",
|
||||
br_code: ButtonRequestType = ButtonRequestType.SignTx,
|
||||
) -> None:
|
||||
await raise_if_not_confirmed(
|
||||
interact(
|
||||
RustLayout(
|
||||
trezorui2.confirm_ethereum_tx(
|
||||
recipient=recipient,
|
||||
total_amount=total_amount,
|
||||
maximum_fee=maximum_fee,
|
||||
items=items,
|
||||
)
|
||||
),
|
||||
br_type,
|
||||
br_code,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def confirm_joint_total(spending_amount: str, total_amount: str) -> None:
|
||||
|
||||
await raise_if_not_confirmed(
|
||||
@ -1114,7 +1138,7 @@ async def confirm_signverify(
|
||||
br_type,
|
||||
"CONFIRM MESSAGE",
|
||||
message,
|
||||
verb_cancel="up_arrow_icon",
|
||||
verb_cancel="^",
|
||||
br_code=BR_TYPE_OTHER,
|
||||
ask_pagination=True,
|
||||
)
|
||||
|
@ -10,6 +10,7 @@ EXCEPTIONS+=( "mnemonic" ) # has NEM in it
|
||||
EXCEPTIONS+=( "workflow" "overflow" ) # has Flo in it
|
||||
EXCEPTIONS+=( "SyntaxError" ) # has Axe in it
|
||||
EXCEPTIONS+=( "DKDNEM" ) # has NEM in it, some sort of weird coincidence
|
||||
EXCEPTIONS+=( "confirm_ethereum_tx" ) # is model-specific, so is in layout/__init__.py instead of ethereum/layout.py
|
||||
|
||||
GREP_ARGS=()
|
||||
for exception in "${EXCEPTIONS[@]}"; do
|
||||
|
Loading…
Reference in New Issue
Block a user