1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-06-28 19:02:34 +00:00

feat(core): introduce a flow for ethereum approve

This commit is contained in:
Ioan Bizău 2025-04-25 08:45:20 +02:00 committed by Ioan Bizău
parent c275b5d995
commit 0ba8173424
30 changed files with 1013 additions and 120 deletions

View File

@ -0,0 +1,199 @@
{
"setup": {
"mnemonic": "alcohol woman abuse must during monitor noble actual mixed trade anger aisle",
"passphrase": ""
},
"tests": [
{
"name": "approve_known_token_known_chain",
"parameters": {
"comment": "Approve",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "3be4aa3820a4afd92664f1d6811610c7205fcb34da3dbacc881f514b04d46276",
"sig_s": "7367fa66ebf7cb0f8497b035d9de6e035cd9c24f04a19b106c62b1cfb96393ce"
},
"skip_models": ["t1"]
},
{
"name": "revoke_known_token_known_chain",
"parameters": {
"comment": "Revoke",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000000",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "eb4a6eb73e657fb4bd9e622f7af7cc49019fdac457a59376d596b86ec1cf8104",
"sig_s": "0cf53c99917d603635686898035eb9f2611ccc0c388e89ec02ec640a9e0e30eb"
},
"skip_models": ["t1"]
},
{
"name": "approve_uniswap_unknown_token",
"parameters": {
"comment": "Approve - Uniswap - unknown token",
"data": "095ea7b3000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "2854b6fd729e66f2fde19da60442612ae775aa3950cf2b7702a925b584b9e520",
"sig_s": "1dae1dfe009ba01edb3b96d61490c2eeea12dc20ef71c31fb846c5b60283310a"
},
"skip_models": ["t1"]
},
{
"name": "revoke_uniswap_unknown_token",
"parameters": {
"comment": "Revoke - Uniswap - unknown token",
"data": "095ea7b3000000000000000000000000e592427a0aece92de3edee1f18e0157c058615640000000000000000000000000000000000000000000000000000000000000000",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x00"
},
"result": {
"sig_v": 37,
"sig_r": "461c9aaa41b851bf64157521d5aa4f367f5af27db243de52545247184a594d24",
"sig_s": "45a6cb062a304f06c4a56e5357ad50381283433d65ea687a7c1f50a221267c4a"
},
"skip_models": ["t1"]
},
{
"name": "approve_unlimited",
"parameters": {
"comment": "Approve - unlimited",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 38,
"sig_r": "34b03573f2436ec3a90f0dc4f0ebe1f2f850cdad1baf17ff6d762210ab0d0e37",
"sig_s": "5906821df079dfb4fb515e64582eda5ca6307e35074f059a7abf8c7a1917362a"
},
"skip_models": ["t1"]
},
{
"name": "approve_unknown_token_unknown_chain",
"parameters": {
"comment": "Approve - unknown token unknown chain",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xdddddddddddddddddddddddddddddddddddddddd",
"chain_id": 200901,
"fake_defs": false,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 401837,
"sig_r": "cf8cb9bd0aa970617c989108e80ee222f5d0af74cef4baa952c63584c8fe713a",
"sig_s": "08253cf6904c7c6d78ff19999bce746a0d97396c196228ee67dbf4e3e8cc3798"
},
"skip_models": ["t1"]
},
{
"name": "revoke_unknown_token_unknown_chain",
"parameters": {
"comment": "Revoke - unknown token unknown chain",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000000",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xdddddddddddddddddddddddddddddddddddddddd",
"chain_id": 200901,
"fake_defs": false,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 401838,
"sig_r": "976c17f8833b8260682ab7b3cfec810f3f9239d53baa0ed622a8cccb2ba9d018",
"sig_s": "3e1072f823e90a029b48a5aaa57630c4b1e9b6808c3af441c8abcb16b5823ca5"
},
"skip_models": ["t1"]
},
{
"name": "approve_unknown_token_known_chain",
"parameters": {
"comment": "Approve - unknown token known chain",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "b10bb751b48d72bbb6d55ecb34800c08f92fda818480f67190d923c5bd73d2c4",
"sig_s": "78fb7fa237bc6bfa0e5bdb96164db4162f41866f2f30b54295d947fe87a813a8"
},
"skip_models": ["t1"]
},
{
"name": "revoke_unknown_token_known_chain",
"parameters": {
"comment": "Revoke - unknown token known chain",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000000",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "21393dee0befb33c52b4916019df6b1bbace9015221e8f32de82f2554a8fad2d",
"sig_s": "42f03f5dd99ed5b2368e85048546468fd9ae962bb0c7b6650bf0a39b9b2bb517"
},
"skip_models": ["t1"]
}
]
}

View File

@ -0,0 +1 @@
Ethereum "approve" flow.

View File

@ -786,6 +786,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__blockhash;
MP_QSTR_words__buying;
MP_QSTR_words__cancel_and_exit;
MP_QSTR_words__chain;
MP_QSTR_words__confirm;
MP_QSTR_words__confirm_fee;
MP_QSTR_words__contains;
@ -822,8 +823,10 @@ static void _librust_qstrs(void) {
MP_QSTR_words__title_success;
MP_QSTR_words__title_summary;
MP_QSTR_words__title_threshold;
MP_QSTR_words__token;
MP_QSTR_words__try_again;
MP_QSTR_words__unknown;
MP_QSTR_words__unlimited;
MP_QSTR_words__unlocked;
MP_QSTR_words__warning;
MP_QSTR_words__writable;
@ -986,6 +989,17 @@ static void _librust_qstrs(void) {
MP_QSTR_eos__vote_for_proxy;
MP_QSTR_eos__voter;
MP_QSTR_ethereum__amount_sent;
MP_QSTR_ethereum__approve;
MP_QSTR_ethereum__approve_amount_allowance;
MP_QSTR_ethereum__approve_chain_id;
MP_QSTR_ethereum__approve_intro;
MP_QSTR_ethereum__approve_intro_revoke;
MP_QSTR_ethereum__approve_intro_title;
MP_QSTR_ethereum__approve_intro_title_revoke;
MP_QSTR_ethereum__approve_revoke;
MP_QSTR_ethereum__approve_revoke_from;
MP_QSTR_ethereum__approve_to;
MP_QSTR_ethereum__approve_unlimited_template;
MP_QSTR_ethereum__contract;
MP_QSTR_ethereum__data_size_template;
MP_QSTR_ethereum__gas_limit;
@ -1143,7 +1157,6 @@ static void _librust_qstrs(void) {
MP_QSTR_solana__stake_question;
MP_QSTR_solana__stake_withdrawal_warning;
MP_QSTR_solana__stake_withdrawal_warning_title;
MP_QSTR_solana__title_token;
MP_QSTR_solana__transaction_contains_unknown_instructions;
MP_QSTR_solana__transaction_fee;
MP_QSTR_solana__transaction_requires_x_signers_template;

View File

@ -943,8 +943,6 @@ pub enum TranslatedString {
#[cfg(feature = "universal_fw")]
solana__multiple_signers = 672, // "Multiple signers"
#[cfg(feature = "universal_fw")]
solana__title_token = 673, // "Token"
#[cfg(feature = "universal_fw")]
solana__transaction_contains_unknown_instructions = 674, // "Transaction contains unknown instructions."
#[cfg(feature = "universal_fw")]
solana__transaction_requires_x_signers_template = 675, // "Transaction requires {0} signers which increases the fee."
@ -1392,6 +1390,31 @@ pub enum TranslatedString {
solana__max_rent_fee = 998, // "Max rent fee"
#[cfg(feature = "universal_fw")]
solana__transaction_fee = 999, // "Transaction fee"
#[cfg(feature = "universal_fw")]
ethereum__approve = 1000, // "Approve"
#[cfg(feature = "universal_fw")]
ethereum__approve_amount_allowance = 1001, // "Amount allowance"
#[cfg(feature = "universal_fw")]
ethereum__approve_chain_id = 1002, // "Chain ID"
#[cfg(feature = "universal_fw")]
ethereum__approve_intro = 1003, // "Review details to approve token spending."
#[cfg(feature = "universal_fw")]
ethereum__approve_intro_title = 1004, // "Token approval"
#[cfg(feature = "universal_fw")]
ethereum__approve_to = 1005, // "Approve to"
#[cfg(feature = "universal_fw")]
ethereum__approve_unlimited_template = 1006, // "Approving unlimited amount of {0}"
words__unlimited = 1007, // "Unlimited"
#[cfg(feature = "universal_fw")]
ethereum__approve_intro_revoke = 1008, // "Review details to revoke token approval."
#[cfg(feature = "universal_fw")]
ethereum__approve_intro_title_revoke = 1009, // "Token revocation"
#[cfg(feature = "universal_fw")]
ethereum__approve_revoke = 1010, // "Revoke"
#[cfg(feature = "universal_fw")]
ethereum__approve_revoke_from = 1011, // "Revoke from"
words__chain = 1012, // "Chain"
words__token = 1013, // "Token"
}
impl TranslatedString {
@ -2328,8 +2351,6 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Self::solana__multiple_signers, "Multiple signers"),
#[cfg(feature = "universal_fw")]
(Self::solana__title_token, "Token"),
#[cfg(feature = "universal_fw")]
(Self::solana__transaction_contains_unknown_instructions, "Transaction contains unknown instructions."),
#[cfg(feature = "universal_fw")]
(Self::solana__transaction_requires_x_signers_template, "Transaction requires {0} signers which increases the fee."),
@ -2777,6 +2798,31 @@ impl TranslatedString {
(Self::solana__max_rent_fee, "Max rent fee"),
#[cfg(feature = "universal_fw")]
(Self::solana__transaction_fee, "Transaction fee"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve, "Approve"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_amount_allowance, "Amount allowance"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_chain_id, "Chain ID"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_intro, "Review details to approve token spending."),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_intro_title, "Token approval"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_to, "Approve to"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_unlimited_template, "Approving unlimited amount of {0}"),
(Self::words__unlimited, "Unlimited"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_intro_revoke, "Review details to revoke token approval."),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_intro_title_revoke, "Token revocation"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_revoke, "Revoke"),
#[cfg(feature = "universal_fw")]
(Self::ethereum__approve_revoke_from, "Revoke from"),
(Self::words__chain, "Chain"),
(Self::words__token, "Token"),
];
#[cfg(feature = "micropython")]
@ -3222,6 +3268,28 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__amount_sent, Self::ethereum__amount_sent),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve, Self::ethereum__approve),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_amount_allowance, Self::ethereum__approve_amount_allowance),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_chain_id, Self::ethereum__approve_chain_id),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_intro, Self::ethereum__approve_intro),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_intro_revoke, Self::ethereum__approve_intro_revoke),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_intro_title, Self::ethereum__approve_intro_title),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_intro_title_revoke, Self::ethereum__approve_intro_title_revoke),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_revoke, Self::ethereum__approve_revoke),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_revoke_from, Self::ethereum__approve_revoke_from),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_to, Self::ethereum__approve_to),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__approve_unlimited_template, Self::ethereum__approve_unlimited_template),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__contract, Self::ethereum__contract),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_ethereum__data_size_template, Self::ethereum__data_size_template),
@ -3878,8 +3946,6 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_solana__stake_withdrawal_warning_title, Self::solana__stake_withdrawal_warning_title),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_solana__title_token, Self::solana__title_token),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_solana__transaction_contains_unknown_instructions, Self::solana__transaction_contains_unknown_instructions),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_solana__transaction_fee, Self::solana__transaction_fee),
@ -4120,6 +4186,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__blockhash, Self::words__blockhash),
(Qstr::MP_QSTR_words__buying, Self::words__buying),
(Qstr::MP_QSTR_words__cancel_and_exit, Self::words__cancel_and_exit),
(Qstr::MP_QSTR_words__chain, Self::words__chain),
(Qstr::MP_QSTR_words__confirm, Self::words__confirm),
(Qstr::MP_QSTR_words__confirm_fee, Self::words__confirm_fee),
(Qstr::MP_QSTR_words__contains, Self::words__contains),
@ -4156,8 +4223,10 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__title_success, Self::words__title_success),
(Qstr::MP_QSTR_words__title_summary, Self::words__title_summary),
(Qstr::MP_QSTR_words__title_threshold, Self::words__title_threshold),
(Qstr::MP_QSTR_words__token, Self::words__token),
(Qstr::MP_QSTR_words__try_again, Self::words__try_again),
(Qstr::MP_QSTR_words__unknown, Self::words__unknown),
(Qstr::MP_QSTR_words__unlimited, Self::words__unlimited),
(Qstr::MP_QSTR_words__unlocked, Self::words__unlocked),
(Qstr::MP_QSTR_words__warning, Self::words__warning),
(Qstr::MP_QSTR_words__writable, Self::words__writable),

View File

@ -339,8 +339,14 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
extern "C" fn new_confirm_summary(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let amount: TString = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
let amount_label: TString = kwargs.get(Qstr::MP_QSTR_amount_label)?.try_into()?;
let amount: Option<TString> = kwargs
.get(Qstr::MP_QSTR_amount)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let amount_label: Option<TString> = kwargs
.get(Qstr::MP_QSTR_amount_label)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let fee: TString = kwargs.get(Qstr::MP_QSTR_fee)?.try_into()?;
let fee_label: TString = kwargs.get(Qstr::MP_QSTR_fee_label)?.try_into()?;
let title: Option<TString> = kwargs
@ -351,6 +357,10 @@ extern "C" fn new_confirm_summary(n_args: usize, args: *const Obj, kwargs: *mut
.get(Qstr::MP_QSTR_account_items)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let account_title: Option<TString> = kwargs
.get(Qstr::MP_QSTR_account_title)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let extra_items: Option<Obj> = kwargs
.get(Qstr::MP_QSTR_extra_items)
.unwrap_or_else(|_| Obj::const_none())
@ -371,6 +381,7 @@ extern "C" fn new_confirm_summary(n_args: usize, args: *const Obj, kwargs: *mut
fee_label,
title,
account_items,
account_title,
extra_items,
extra_title,
verb_cancel,
@ -1315,12 +1326,13 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// def confirm_summary(
/// *,
/// amount: str,
/// amount_label: str,
/// amount: str | None,
/// amount_label: str | None,
/// fee: str,
/// fee_label: str,
/// title: str | None = None,
/// account_items: Iterable[tuple[str, str]] | None = None,
/// account_title: str | None = None,
/// extra_items: Iterable[tuple[str, str]] | None = None,
/// extra_title: str | None = None,
/// verb_cancel: str | None = None,

View File

@ -427,23 +427,29 @@ impl FirmwareUI for UIBolt {
}
fn confirm_summary(
amount: TString<'static>,
amount_label: TString<'static>,
amount: Option<TString<'static>>,
amount_label: Option<TString<'static>>,
fee: TString<'static>,
fee_label: TString<'static>,
title: Option<TString<'static>>,
account_items: Option<Obj>,
_account_title: Option<TString<'static>>,
extra_items: Option<Obj>,
_extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
let info_button: bool = account_items.is_some() || extra_items.is_some();
let paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_NORMAL, amount_label).no_break(),
Paragraph::new(&theme::TEXT_MONO, amount),
Paragraph::new(&theme::TEXT_NORMAL, fee_label).no_break(),
Paragraph::new(&theme::TEXT_MONO, fee),
]);
let mut paragraphs = ParagraphVecShort::new();
if let Some(amount) = amount {
if let Some(amount_label) = amount_label {
paragraphs
.add(Paragraph::new(&theme::TEXT_NORMAL, amount_label).no_break())
.add(Paragraph::new(&theme::TEXT_MONO, amount));
}
}
paragraphs
.add(Paragraph::new(&theme::TEXT_NORMAL, fee_label).no_break())
.add(Paragraph::new(&theme::TEXT_MONO, fee));
let mut page = ButtonPage::new(paragraphs.into_paragraphs(), theme::BG)
.with_hold()?

View File

@ -484,12 +484,13 @@ impl FirmwareUI for UICaesar {
}
fn confirm_summary(
amount: TString<'static>,
amount_label: TString<'static>,
amount: Option<TString<'static>>,
amount_label: Option<TString<'static>>,
fee: TString<'static>,
fee_label: TString<'static>,
title: Option<TString<'static>>,
account_items: Option<Obj>,
account_title: Option<TString<'static>>,
extra_items: Option<Obj>,
extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
@ -502,7 +503,9 @@ impl FirmwareUI for UICaesar {
unwrap!(info_pages.push((extra_title, info)));
}
if let Some(info) = account_items {
unwrap!(info_pages.push((TR::confirm_total__title_sending_from.into(), info)));
let account_title =
account_title.unwrap_or(TR::confirm_total__title_sending_from.into());
unwrap!(info_pages.push((account_title, info)));
}
// button layouts and actions
@ -553,14 +556,23 @@ impl FirmwareUI for UICaesar {
if let Some(title) = title {
ops = ops.text(title, fonts::FONT_BOLD_UPPER).newline();
}
ops = ops
.text(amount_label, fonts::FONT_BOLD)
.newline()
.text(amount, fonts::FONT_MONO);
let mut has_amount = false;
if let Some(amount) = amount {
if let Some(amount_label) = amount_label {
has_amount = true;
ops = ops
.text(amount_label, fonts::FONT_BOLD)
.newline()
.text(amount, fonts::FONT_MONO);
}
}
if !fee_label.is_empty() || !fee.is_empty() {
if has_amount {
ops = ops.newline();
}
ops = ops
.newline()
.newline()
.text(fee_label, fonts::FONT_BOLD)
.newline()
@ -1298,10 +1310,14 @@ fn content_in_button_page<T: Component + Paginate + MaybeTrace + 'static>(
// Left button - icon, text or nothing.
let cancel_btn = verb_cancel.map(ButtonDetails::from_text_possible_icon);
// Right button - text or nothing.
// Right button - down arrow, text or nothing.
// Optional HoldToConfirm
let mut confirm_btn = if !verb.is_empty() {
Some(ButtonDetails::text(verb))
if verb == TString::Str("V") {
Some(ButtonDetails::down_arrow_icon_wide())
} else {
Some(ButtonDetails::text(verb))
}
} else {
None
};

View File

@ -358,12 +358,13 @@ impl FirmwareUI for UIDelizia {
}
fn confirm_summary(
amount: TString<'static>,
amount_label: TString<'static>,
amount: Option<TString<'static>>,
amount_label: Option<TString<'static>>,
fee: TString<'static>,
fee_label: TString<'static>,
title: Option<TString<'static>>,
account_items: Option<Obj>,
account_title: Option<TString<'static>>,
extra_items: Option<Obj>,
extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
@ -371,13 +372,17 @@ impl FirmwareUI for UIDelizia {
let mut summary_params = ShowInfoParams::new(title.unwrap_or(TString::empty()))
.with_menu_button()
.with_swipeup_footer(None);
summary_params = unwrap!(summary_params.add(amount_label, amount));
if let Some(amount) = amount {
if let Some(amount_label) = amount_label {
summary_params = unwrap!(summary_params.add(amount_label, amount));
}
}
summary_params = unwrap!(summary_params.add(fee_label, fee));
// collect available info
let account_params = if let Some(items) = account_items {
let mut account_params =
ShowInfoParams::new(TR::send__send_from.into()).with_cancel_button();
let account_title = account_title.unwrap_or(TR::send__send_from.into());
let mut account_params = ShowInfoParams::new(account_title).with_cancel_button();
for pair in IterBuf::new().try_iterate(items)? {
let [label, value]: [TString; 2] = util::iter_into_array(pair)?;
account_params = unwrap!(account_params.add(label, value));

View File

@ -130,13 +130,14 @@ pub trait FirmwareUI {
#[allow(clippy::too_many_arguments)]
fn confirm_summary(
amount: TString<'static>,
amount_label: TString<'static>,
amount: Option<TString<'static>>,
amount_label: Option<TString<'static>>,
fee: TString<'static>,
fee_label: TString<'static>,
title: Option<TString<'static>>,
account_items: Option<Obj>, // TODO: replace Obj
extra_items: Option<Obj>, // TODO: replace Obj
account_title: Option<TString<'static>>,
extra_items: Option<Obj>, // TODO: replace Obj
extra_title: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error>;

View File

@ -451,11 +451,11 @@ Q(apps.ethereum.helpers)
Q(apps.ethereum.keychain)
Q(apps.ethereum.layout)
Q(apps.ethereum.networks)
Q(apps.ethereum.sc_constants)
Q(apps.ethereum.sign_message)
Q(apps.ethereum.sign_tx)
Q(apps.ethereum.sign_tx_eip1559)
Q(apps.ethereum.sign_typed_data)
Q(apps.ethereum.staking_tx_constants)
Q(apps.ethereum.tokens)
Q(apps.ethereum.verify_message)
Q(apps.monero)
@ -647,6 +647,7 @@ Q(readwriter)
Q(remove_resident_credential)
Q(resident_credentials)
Q(ripple)
Q(sc_constants)
Q(seed)
Q(serialize)
Q(serialize_messages)
@ -657,7 +658,6 @@ Q(sign_typed_data)
Q(signer)
Q(signing)
Q(solana)
Q(staking_tx_constants)
Q(state)
Q(stellar)
Q(step_01_init_transaction)

View File

@ -271,12 +271,13 @@ def confirm_reset_device(recovery: bool) -> LayoutObj[UiResult]:
# rust/src/ui/api/firmware_micropython.rs
def confirm_summary(
*,
amount: str,
amount_label: str,
amount: str | None,
amount_label: str | None,
fee: str,
fee_label: str,
title: str | None = None,
account_items: Iterable[tuple[str, str]] | None = None,
account_title: str | None = None,
extra_items: Iterable[tuple[str, str]] | None = None,
extra_title: str | None = None,
verb_cancel: str | None = None,

View File

@ -286,6 +286,17 @@ class TR:
eos__vote_for_proxy: str = "Vote for proxy"
eos__voter: str = "Voter:"
ethereum__amount_sent: str = "Amount sent:"
ethereum__approve: str = "Approve"
ethereum__approve_amount_allowance: str = "Amount allowance"
ethereum__approve_chain_id: str = "Chain ID"
ethereum__approve_intro: str = "Review details to approve token spending."
ethereum__approve_intro_revoke: str = "Review details to revoke token approval."
ethereum__approve_intro_title: str = "Token approval"
ethereum__approve_intro_title_revoke: str = "Token revocation"
ethereum__approve_revoke: str = "Revoke"
ethereum__approve_revoke_from: str = "Revoke from"
ethereum__approve_to: str = "Approve to"
ethereum__approve_unlimited_template: str = "Approving unlimited amount of {0}"
ethereum__contract: str = "Contract:"
ethereum__data_size_template: str = "Size: {0} bytes"
ethereum__gas_limit: str = "Gas limit"
@ -785,7 +796,6 @@ class TR:
solana__stake_question: str = "Stake SOL?"
solana__stake_withdrawal_warning: str = "The current wallet isn't the SOL staking withdraw authority."
solana__stake_withdrawal_warning_title: str = "Withdraw authority address"
solana__title_token: str = "Token"
solana__transaction_contains_unknown_instructions: str = "Transaction contains unknown instructions."
solana__transaction_fee: str = "Transaction fee"
solana__transaction_requires_x_signers_template: str = "Transaction requires {0} signers which increases the fee."
@ -937,6 +947,7 @@ class TR:
words__blockhash: str = "Blockhash"
words__buying: str = "Buying"
words__cancel_and_exit: str = "Cancel and exit"
words__chain: str = "Chain"
words__confirm: str = "Confirm"
words__confirm_fee: str = "Confirm fee"
words__contains: str = "Contains"
@ -973,8 +984,10 @@ class TR:
words__title_success: str = "Success"
words__title_summary: str = "Summary"
words__title_threshold: str = "Threshold"
words__token: str = "Token"
words__try_again: str = "Try again."
words__unknown: str = "Unknown"
words__unlimited: str = "Unlimited"
words__unlocked: str = "Unlocked"
words__warning: str = "Warning"
words__writable: str = "Writable"

View File

@ -27,6 +27,54 @@ if TYPE_CHECKING:
)
async def require_confirm_approve(
to_bytes: bytes,
value: int | None,
address_n: list[int],
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
chain_id: int,
network: EthereumNetworkInfo,
token: EthereumTokenInfo,
token_address: bytes,
chunkify: bool,
) -> None:
from trezor.ui.layouts import confirm_ethereum_approve
from apps.ethereum.sc_constants import APPROVE_KNOWN_ADDRESSES as KNOWN_ADDRESSES
from . import networks, tokens
if to_bytes in KNOWN_ADDRESSES:
to_str = KNOWN_ADDRESSES[to_bytes]
chunkify = False
else:
to_str = address_from_bytes(to_bytes, network)
chain_id_str = f"{chain_id} ({hex(chain_id)})"
token_address_str = address_from_bytes(token_address, network)
total_amount = (
format_ethereum_amount(value, token, network) if value is not None else None
)
account, account_path = get_account_and_path(address_n)
await confirm_ethereum_approve(
to_str,
token is tokens.UNKNOWN_TOKEN,
token_address_str,
token.symbol,
network is networks.UNKNOWN_NETWORK,
chain_id_str,
network.name,
value == 0,
total_amount,
account,
account_path,
maximum_fee,
fee_info_items,
chunkify=chunkify,
)
async def require_confirm_tx(
to_bytes: bytes,
value: int,
@ -141,36 +189,30 @@ async def require_confirm_claim(
)
async def require_confirm_unknown_token(address_bytes: bytes) -> None:
from ubinascii import hexlify
from trezor.ui.layouts import (
confirm_address,
confirm_ethereum_unknown_contract_warning,
)
async def require_confirm_unknown_token() -> None:
from trezor.ui.layouts import confirm_ethereum_unknown_contract_warning
await confirm_ethereum_unknown_contract_warning()
contract_address_hex = "0x" + hexlify(address_bytes).decode()
await confirm_address(
TR.words__address,
contract_address_hex,
subtitle=TR.ethereum__token_contract,
verb=TR.buttons__continue,
br_name="unknown_token",
br_code=ButtonRequestType.SignTx,
)
def require_confirm_address(address_bytes: bytes) -> Awaitable[None]:
def require_confirm_address(
address_bytes: bytes,
title: str | None = None,
subtitle: str | None = None,
verb: str | None = None,
br_name: str | None = None,
) -> Awaitable[None]:
from ubinascii import hexlify
from trezor.ui.layouts import confirm_address
address_hex = "0x" + hexlify(address_bytes).decode()
return confirm_address(
TR.ethereum__title_signing_address,
title or TR.ethereum__title_signing_address,
address_hex,
subtitle=subtitle,
verb=verb,
br_name=br_name,
br_code=ButtonRequestType.SignTx,
)

View File

@ -4,12 +4,20 @@ from ubinascii import unhexlify
# smart contract 'data' field lengths in bytes
SC_FUNC_SIG_BYTES = const(4)
SC_ARGUMENT_BYTES = const(32)
SC_ARGUMENT_ADDRESS_BYTES = const(20)
# staking operations function signatures
assert SC_ARGUMENT_ADDRESS_BYTES <= SC_ARGUMENT_BYTES
# Known ERC-20 functions
SC_FUNC_SIG_TRANSFER = unhexlify("a9059cbb")
SC_FUNC_SIG_APPROVE = unhexlify("095ea7b3")
SC_FUNC_SIG_STAKE = unhexlify("3a29dbae")
SC_FUNC_SIG_UNSTAKE = unhexlify("76ec871c")
SC_FUNC_SIG_CLAIM = unhexlify("33986ffa")
# Everstake staking
# addresses for pool (stake/unstake) and accounting (claim) operations
ADDRESSES_POOL = (
unhexlify("AFA848357154a6a624686b348303EF9a13F63264"), # holesky testnet
@ -19,3 +27,11 @@ ADDRESSES_ACCOUNTING = (
unhexlify("624087DD1904ab122A32878Ce9e933C7071F53B9"), # holesky testnet
unhexlify("7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e"), # mainnet
)
# Approve known addresses
# This should eventually grow into a more comprehensive database and stored in some other way,
# but for now let's just keep a few known addresses here!
APPROVE_KNOWN_ADDRESSES = {
unhexlify("e592427a0aece92de3edee1f18e0157c05861564"): "Uniswap V3 Router",
}

View File

@ -5,7 +5,7 @@ from trezor.messages import EthereumTxRequest
from trezor.utils import BufferReader
from trezor.wire import DataError
from apps.ethereum import staking_tx_constants as constants
from apps.ethereum import sc_constants as constants
from .helpers import bytes_from_address
from .keychain import with_keychain_from_chain_id
@ -123,31 +123,56 @@ async def confirm_tx_data(
data_total_len: int,
) -> None:
# function distinguishes between staking / smart contracts / regular transactions
from .layout import require_confirm_other_data, require_confirm_tx
from .layout import (
require_confirm_approve,
require_confirm_other_data,
require_confirm_tx,
)
if await handle_staking(msg, defs.network, address_bytes, maximum_fee, fee_items):
return
# Handle ERC-20, currently only 'transfer' function
token, recipient, value = await _handle_erc20_transfer(msg, defs, address_bytes)
is_contract_interaction = token is None and data_total_len > 0
if is_contract_interaction:
await require_confirm_other_data(msg.data_initial_chunk, data_total_len)
await require_confirm_tx(
recipient,
value,
msg.address_n,
maximum_fee,
fee_items,
defs.network,
token,
is_contract_interaction=is_contract_interaction,
chunkify=bool(msg.chunkify),
# Handle ERC-20 known functions
token, token_address, func_sig, recipient, value = await _handle_erc20(
msg, defs, address_bytes
)
if func_sig == constants.SC_FUNC_SIG_APPROVE:
assert token
assert token_address
await require_confirm_approve(
recipient,
value,
msg.address_n,
maximum_fee,
fee_items,
msg.chain_id,
defs.network,
token,
token_address,
chunkify=bool(msg.chunkify),
)
else:
assert func_sig == constants.SC_FUNC_SIG_TRANSFER or func_sig is None
is_contract_interaction = token is None and data_total_len > 0
if is_contract_interaction:
await require_confirm_other_data(msg.data_initial_chunk, data_total_len)
assert value is not None
await require_confirm_tx(
recipient,
value,
msg.address_n,
maximum_fee,
fee_items,
defs.network,
token,
is_contract_interaction=is_contract_interaction,
chunkify=bool(msg.chunkify),
)
async def handle_staking(
msg: MsgInSignTx,
@ -191,16 +216,27 @@ async def handle_staking(
return False
async def _handle_erc20_transfer(
async def _handle_erc20(
msg: MsgInSignTx,
definitions: Definitions,
address_bytes: bytes,
) -> tuple[EthereumTokenInfo | None, bytes, int]:
from . import tokens
from .layout import require_confirm_unknown_token
) -> tuple[EthereumTokenInfo | None, bytes | None, bytes | None, bytes, int | None]:
from trezor import TR
from . import tokens
from .layout import require_confirm_address, require_confirm_unknown_token
# local_cache_attribute
data_initial_chunk = msg.data_initial_chunk
SC_FUNC_SIG_BYTES = constants.SC_FUNC_SIG_BYTES
SC_ARGUMENT_BYTES = constants.SC_ARGUMENT_BYTES
SC_ARGUMENT_ADDRESS_BYTES = constants.SC_ARGUMENT_ADDRESS_BYTES
SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
SC_FUNC_SIG_TRANSFER = constants.SC_FUNC_SIG_TRANSFER
data_initial_chunk = msg.data_initial_chunk # local_cache_attribute
token = None
token_address = None
func_sig = None
recipient = address_bytes
value = int.from_bytes(msg.value, "big")
if (
@ -208,17 +244,51 @@ async def _handle_erc20_transfer(
and len(msg.value) == 0
and msg.data_length == 68
and len(data_initial_chunk) == 68
and data_initial_chunk[:16]
== b"\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
):
data_reader = BufferReader(data_initial_chunk)
if data_reader.remaining_count() < SC_FUNC_SIG_BYTES:
return token, token_address, func_sig, recipient, value
func_sig = data_reader.read_memoryview(SC_FUNC_SIG_BYTES)
if func_sig in (SC_FUNC_SIG_TRANSFER, SC_FUNC_SIG_APPROVE):
# The two functions happen to have the exact same parameters, so we treat them together.
# This will need to be made into a more generic solution eventually.
# arg0: address, Address, 20 bytes (left padded with zeroes)
# arg1: value, uint256, 32 bytes
if data_reader.remaining_count() < SC_FUNC_SIG_BYTES * 2:
return token, token_address, None, recipient, value
arg0 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
assert all(
byte == 0
for byte in arg0[: SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES]
)
recipient = bytes(arg0[SC_ARGUMENT_BYTES - SC_ARGUMENT_ADDRESS_BYTES :])
arg1 = data_reader.read_memoryview(SC_ARGUMENT_BYTES)
if func_sig == SC_FUNC_SIG_APPROVE and all(byte == 255 for byte in arg1):
# "Unlimited" approval (all bits set) is a special case
# which we encode as value=None internally.
value = None
else:
value = int.from_bytes(arg1, "big")
token = definitions.get_token(address_bytes)
recipient = data_initial_chunk[16:36]
value = int.from_bytes(data_initial_chunk[36:68], "big")
token_address = address_bytes
if token is tokens.UNKNOWN_TOKEN:
await require_confirm_unknown_token(address_bytes)
await require_confirm_unknown_token()
if func_sig != SC_FUNC_SIG_APPROVE:
# For unknown tokens we also show the token address immediately after the warning
# except in the case of the "approve" flow which shows the token address later on!
await require_confirm_address(
address_bytes,
TR.words__address,
TR.ethereum__token_contract,
TR.buttons__continue,
"unknown_token",
)
return token, recipient, value
return token, token_address, func_sig, recipient, value
def _get_total_length(msg: EthereumSignTx, data_total: int) -> int:

View File

@ -325,7 +325,7 @@ async def confirm_token_transfer(
value = token.name + "\n" + base58.encode(token.mint)
await confirm_value(
title=TR.solana__title_token,
title=TR.words__token,
value=value,
description="",
br_name="confirm_token_address",

View File

@ -637,14 +637,14 @@ def confirm_address(
description: str | None = None,
verb: str | None = None,
chunkify: bool = True,
br_name: str = "confirm_address",
br_name: str | None = None,
br_code: ButtonRequestType = BR_CODE_OTHER,
) -> Awaitable[None]:
return confirm_value(
title,
address,
description or subtitle or "",
br_name,
br_name or "confirm_address",
br_code,
subtitle=None,
verb=(verb or TR.buttons__confirm),
@ -700,6 +700,7 @@ def confirm_value(
is_data: bool = True,
info_items: Iterable[tuple[str, str]] | None = None,
info_title: str | None = None,
chunkify: bool = False,
chunkify_info: bool = False,
) -> Awaitable[None]:
"""General confirmation dialog, used by many other confirm_* functions."""
@ -720,6 +721,7 @@ def confirm_value(
value=value,
description=description,
is_data=is_data,
chunkify=chunkify,
subtitle=subtitle,
verb=verb,
verb_cancel=verb_cancel,
@ -793,8 +795,8 @@ def confirm_total(
def _confirm_summary(
amount: str,
amount_label: str,
amount: str | None,
amount_label: str | None,
fee: str,
fee_label: str,
title: str | None = None,
@ -898,6 +900,109 @@ if not utils.BITCOIN_ONLY:
else:
break
async def confirm_ethereum_approve(
recipient: str,
is_unknown_token: bool,
token_address: str,
token_symbol: str,
is_unknown_network: bool,
chain_id: str,
network_name: str,
is_revoke: bool,
total_amount: str | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
chunkify: bool = False,
) -> None:
await confirm_value(
(
TR.ethereum__approve_intro_title_revoke
if is_revoke
else TR.ethereum__approve_intro_title
),
(
TR.ethereum__approve_intro_revoke
if is_revoke
else TR.ethereum__approve_intro
),
None,
verb=TR.buttons__continue,
is_data=False,
br_name="confirm_ethereum_approve",
)
await confirm_value(
TR.ethereum__approve_revoke_from if is_revoke else TR.ethereum__approve_to,
recipient,
None,
verb=TR.buttons__continue,
br_name="confirm_ethereum_approve",
chunkify=chunkify,
)
if total_amount is None:
await show_warning(
"confirm_ethereum_approve",
TR.ethereum__approve_unlimited_template.format(token_symbol),
)
if is_unknown_token:
await confirm_value(
TR.words__address,
token_address,
None,
verb=TR.buttons__continue,
subtitle=TR.ethereum__token_contract,
br_name="confirm_ethereum_approve",
chunkify=chunkify,
)
if is_unknown_network:
assert is_unknown_token
await confirm_value(
TR.ethereum__approve_chain_id,
chain_id,
None,
verb=TR.buttons__continue,
br_name="confirm_ethereum_approve",
)
properties = (
[(TR.words__token, token_symbol)]
if is_revoke
else [
(
f"{TR.ethereum__approve_amount_allowance}:",
total_amount or TR.words__unlimited,
)
]
)
if not is_unknown_network:
properties.append((f"{TR.words__chain}:", network_name))
await confirm_properties(
"confirm_ethereum_approve",
TR.ethereum__approve_revoke if is_revoke else TR.ethereum__approve,
properties,
False,
)
account_items = []
if account_path:
account_items.append((TR.address_details__derivation_path, account_path))
await _confirm_summary(
None,
None,
maximum_fee,
TR.send__maximum_fee,
TR.words__title_summary,
account_items,
fee_info_items,
TR.confirm_total__title_fee,
)
async def confirm_ethereum_staking_tx(
title: str,
intro_question: str,

View File

@ -672,11 +672,11 @@ def confirm_address(
description: str | None = None,
verb: str | None = None,
chunkify: bool = True,
br_name: str = "confirm_address",
br_name: str | None = None,
br_code: ButtonRequestType = BR_CODE_OTHER,
) -> Awaitable[None]:
return confirm_blob(
br_name,
br_name or "confirm_address",
subtitle or title,
address,
description,
@ -763,6 +763,7 @@ async def confirm_value(
hold: bool = False,
is_data: bool = True,
info_items: Iterable[tuple[str, str]] | None = None,
chunkify: bool = False,
chunkify_info: bool = False,
) -> None:
"""General confirmation dialog, used by many other confirm_* functions."""
@ -781,6 +782,7 @@ async def confirm_value(
info=False,
hold=hold,
is_data=is_data,
chunkify=chunkify,
),
br_name,
br_code,
@ -873,6 +875,117 @@ if not utils.BITCOIN_ONLY:
"unknown_contract_warning", TR.ethereum__unknown_contract_address_short
)
async def confirm_ethereum_approve(
recipient: str,
is_unknown_token: bool,
token_address: str,
token_symbol: str,
is_unknown_network: bool,
chain_id: str,
network_name: str,
is_revoke: bool,
total_amount: str | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
chunkify: bool = False,
) -> None:
await confirm_value(
(
TR.ethereum__approve_intro_title_revoke
if is_revoke
else TR.ethereum__approve_intro_title
),
(
TR.ethereum__approve_intro_revoke
if is_revoke
else TR.ethereum__approve_intro
),
None,
verb=TR.buttons__continue,
hold=False,
is_data=False,
br_name="confirm_ethereum_approve",
)
await confirm_value(
TR.ethereum__approve_revoke_from if is_revoke else TR.ethereum__approve_to,
recipient,
None,
verb=TR.buttons__continue,
hold=False,
br_name="confirm_ethereum_approve",
chunkify=chunkify,
)
if total_amount is None:
await show_warning(
"confirm_ethereum_approve",
TR.ethereum__approve_unlimited_template.format(token_symbol),
TR.words__continue_anyway_question,
)
if is_unknown_token:
await confirm_value(
TR.ethereum__token_contract + " | " + TR.words__address,
token_address,
None,
verb="V",
hold=False,
br_name="confirm_ethereum_approve",
chunkify=chunkify,
)
if is_unknown_network:
assert is_unknown_token
await confirm_value(
TR.ethereum__approve_chain_id,
chain_id,
None,
verb="V",
hold=False,
br_name="confirm_ethereum_approve",
)
properties = (
[(TR.words__token, token_symbol)]
if is_revoke
else [
(
TR.ethereum__approve_amount_allowance,
total_amount or TR.words__unlimited,
)
]
)
if not is_unknown_network:
properties.append((TR.words__chain, network_name))
await confirm_properties(
"confirm_ethereum_approve",
TR.ethereum__approve_revoke if is_revoke else TR.ethereum__approve,
properties,
False,
)
account_items = []
if account_path:
account_items.append((TR.address_details__derivation_path, account_path))
await raise_if_not_confirmed(
trezorui_api.confirm_summary(
amount=None,
amount_label=None,
fee=maximum_fee,
fee_label=f"{TR.send__maximum_fee}:",
title=TR.words__title_summary,
account_items=[(f"{k}:", v) for (k, v) in account_items],
account_title=TR.address_details__account_info,
extra_items=fee_info_items,
extra_title=TR.confirm_total__title_fee,
),
br_name="confirm_ethereum_approve",
)
async def confirm_ethereum_staking_tx(
title: str,
intro_question: str,

View File

@ -557,14 +557,14 @@ def confirm_address(
description: str | None = None,
verb: str | None = None,
chunkify: bool = True,
br_name: str = "confirm_address",
br_name: str | None = None,
br_code: ButtonRequestType = BR_CODE_OTHER,
) -> Awaitable[None]:
return confirm_value(
title,
address,
description or "",
br_name,
br_name or "confirm_address",
br_code,
subtitle=subtitle,
verb=(verb or TR.buttons__confirm),
@ -713,8 +713,8 @@ def confirm_total(
def _confirm_summary(
amount: str,
amount_label: str,
amount: str | None,
amount_label: str | None,
fee: str,
fee_label: str,
title: str | None = None,
@ -797,6 +797,105 @@ if not utils.BITCOIN_ONLY:
None,
)
async def confirm_ethereum_approve(
recipient: str,
is_unknown_token: bool,
token_address: str,
token_symbol: str,
is_unknown_network: bool,
chain_id: str,
network_name: str,
is_revoke: bool,
total_amount: str | None,
account: str | None,
account_path: str | None,
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
chunkify: bool = False,
) -> None:
await confirm_value(
(
TR.ethereum__approve_intro_title_revoke
if is_revoke
else TR.ethereum__approve_intro_title
),
(
TR.ethereum__approve_intro_revoke
if is_revoke
else TR.ethereum__approve_intro
),
"",
is_data=False,
br_name="confirm_ethereum_approve",
)
await confirm_value(
TR.ethereum__approve_revoke_from if is_revoke else TR.ethereum__approve_to,
recipient,
"",
chunkify=chunkify,
br_name="confirm_ethereum_approve",
)
if total_amount is None:
await show_warning(
"confirm_ethereum_approve",
TR.ethereum__approve_unlimited_template.format(token_symbol),
)
if is_unknown_token:
await confirm_value(
TR.words__address,
token_address,
"",
subtitle=TR.ethereum__token_contract,
chunkify=chunkify,
br_name="confirm_ethereum_approve",
)
if is_unknown_network:
assert is_unknown_token
await confirm_value(
TR.ethereum__approve_chain_id,
chain_id,
"",
br_name="confirm_ethereum_approve",
)
properties = (
[(TR.words__token, token_symbol)]
if is_revoke
else [
(
TR.ethereum__approve_amount_allowance,
total_amount or TR.words__unlimited,
)
]
)
if not is_unknown_network:
properties.append((TR.words__chain, network_name))
await confirm_properties(
"confirm_ethereum_approve",
TR.ethereum__approve_revoke if is_revoke else TR.ethereum__approve,
properties,
False,
)
account_items = []
if account_path:
account_items.append((TR.address_details__derivation_path, account_path))
await _confirm_summary(
None,
None,
maximum_fee,
TR.send__maximum_fee,
TR.words__title_summary,
account_items,
fee_info_items,
TR.confirm_total__title_fee,
)
async def confirm_ethereum_staking_tx(
title: str,
intro_question: str,

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Hlasovat pro proxy",
"eos__voter": "Hlasující osoba:",
"ethereum__amount_sent": "Odeslaná částka:",
"ethereum__approve": "Povolit",
"ethereum__approve_amount_allowance": "Limit částky",
"ethereum__approve_chain_id": "Chain ID",
"ethereum__approve_intro_title": "Schválení tokenu",
"ethereum__approve_intro": "Zkontrolujte data a povolte útratu tokenu.",
"ethereum__approve_intro_title_revoke": "Odvolání tokenu",
"ethereum__approve_intro_revoke": "Zkontrolujte a zrušte povolení tokenu.",
"ethereum__approve_revoke": "Zrušit",
"ethereum__approve_revoke_from": "Zrušit pro",
"ethereum__approve_to": "Povolit pro",
"ethereum__approve_unlimited_template": "Povolujete neomezenou částku {0}",
"ethereum__contract": "Kontrakt:",
"ethereum__data_size_template": "Velikost: {0} bajtů",
"ethereum__gas_limit": "Limit gasu",
@ -812,7 +823,6 @@
"solana__stake_question": "Stakovat SOL?",
"solana__stake_withdrawal_warning": "Tato pěněženka nemůže být použita pro výběr z tohoto stakingu.",
"solana__stake_withdrawal_warning_title": "Adresa autority oprávněné k výběru",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "Transakce obsahuje neznámý pokyn.",
"solana__transaction_requires_x_signers_template": "Transakce vyžaduje {0} podepisujících osob, což zvyšuje poplatek.",
"solana__unstake": "Zrušit staking",
@ -963,6 +973,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Nákup",
"words__cancel_and_exit": "Zrušit a ukončit",
"words__chain": "Řetězec",
"words__confirm": "Potvrdit",
"words__confirm_fee": "Potvrdit poplatek",
"words__contains": "Obsahuje",
@ -999,8 +1010,10 @@
"words__title_success": "Hotovo",
"words__title_summary": "Souhrn",
"words__title_threshold": "Části pro obnovu",
"words__token": "Token",
"words__try_again": "Zkuste to znovu.",
"words__unknown": "Neznámé",
"words__unlimited": "Neomezeně",
"words__unlocked": "Odemčeno",
"words__warning": "Varování",
"words__writable": "Zapisovatelné",

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Vote für Proxy",
"eos__voter": "Voter:",
"ethereum__amount_sent": "Gesendeter Betrag:",
"ethereum__approve": "Zulassen",
"ethereum__approve_amount_allowance": "Betragslimit",
"ethereum__approve_chain_id": "Chain ID",
"ethereum__approve_intro_title": "Tokenfreigabe",
"ethereum__approve_intro": "Prüfe Details u. lasse Token-Ausgaben zu.",
"ethereum__approve_intro_title_revoke": "Token-Widerruf",
"ethereum__approve_intro_revoke": "Prüfe Details u. widerrufe Tokenfreigabe.",
"ethereum__approve_revoke": "Entziehen",
"ethereum__approve_revoke_from": "Entziehen von",
"ethereum__approve_to": "Zulassen für",
"ethereum__approve_unlimited_template": "Unbegrenzten Betrag von {0} zulassen",
"ethereum__contract": "Kontrakt:",
"ethereum__data_size_template": "Größe: {0} Bytes",
"ethereum__gas_limit": "Gas-Grenze",
@ -812,7 +823,6 @@
"solana__stake_question": "SOL Staken?",
"solana__stake_withdrawal_warning": "Diese Wallet hat keine SOL-Staking Auszahlungsberechtigung.",
"solana__stake_withdrawal_warning_title": "Auszahlungsautoritätsadresse",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "Transaktion enthält unbekannte Anweisungen.",
"solana__transaction_requires_x_signers_template": "Transaktion erfordert {0} Unterzeichner. Dadurch steigt die Gebühr.",
"solana__unstake": "Entstaken",
@ -963,6 +973,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Kaufen",
"words__cancel_and_exit": "Abbrechen und schließen",
"words__chain": "Chain",
"words__confirm": "Bestätigen",
"words__confirm_fee": "Gebühr bestätigen",
"words__contains": "Enthält",
@ -999,8 +1010,10 @@
"words__title_success": "Erfolg",
"words__title_summary": "Zusammenfassung",
"words__title_threshold": "Schwelle",
"words__token": "Token",
"words__try_again": "Versuche es erneut.",
"words__unknown": "Unbekannt",
"words__unlimited": "Unbegrenzt",
"words__unlocked": "Entsperrt",
"words__warning": "Warnung",
"words__writable": "Beschreibbar",

View File

@ -288,6 +288,17 @@
"eos__vote_for_proxy": "Vote for proxy",
"eos__voter": "Voter:",
"ethereum__amount_sent": "Amount sent:",
"ethereum__approve": "Approve",
"ethereum__approve_amount_allowance": "Amount allowance",
"ethereum__approve_chain_id": "Chain ID",
"ethereum__approve_intro_title": "Token approval",
"ethereum__approve_intro": "Review details to approve token spending.",
"ethereum__approve_intro_title_revoke": "Token revocation",
"ethereum__approve_intro_revoke": "Review details to revoke token approval.",
"ethereum__approve_revoke": "Revoke",
"ethereum__approve_revoke_from": "Revoke from",
"ethereum__approve_to": "Approve to",
"ethereum__approve_unlimited_template": "Approving unlimited amount of {0}",
"ethereum__contract": "Contract:",
"ethereum__data_size_template": "Size: {0} bytes",
"ethereum__gas_limit": "Gas limit",
@ -787,7 +798,6 @@
"solana__stake_question": "Stake SOL?",
"solana__stake_withdrawal_warning": "The current wallet isn't the SOL staking withdraw authority.",
"solana__stake_withdrawal_warning_title": "Withdraw authority address",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "Transaction contains unknown instructions.",
"solana__transaction_fee": "Transaction fee",
"solana__transaction_requires_x_signers_template": "Transaction requires {0} signers which increases the fee.",
@ -939,6 +949,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Buying",
"words__cancel_and_exit": "Cancel and exit",
"words__chain": "Chain",
"words__confirm": "Confirm",
"words__confirm_fee": "Confirm fee",
"words__contains": "Contains",
@ -975,8 +986,10 @@
"words__title_success": "Success",
"words__title_summary": "Summary",
"words__title_threshold": "Threshold",
"words__token": "Token",
"words__try_again": "Try again.",
"words__unknown": "Unknown",
"words__unlimited": "Unlimited",
"words__unlocked": "Unlocked",
"words__warning": "Warning",
"words__writable": "Writable",

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Votar por representación",
"eos__voter": "Votante:",
"ethereum__amount_sent": "Importe enviado:",
"ethereum__approve": "Aprobar",
"ethereum__approve_amount_allowance": "Cantidad autorizada",
"ethereum__approve_chain_id": "ID de cadena",
"ethereum__approve_intro_title": "Aprobación de tokens",
"ethereum__approve_intro": "Revisa los detalles y permite gasto de token.",
"ethereum__approve_intro_title_revoke": "Revocación de tokens",
"ethereum__approve_intro_revoke": "Revisa los detalles y cancela la aprobación de token.",
"ethereum__approve_revoke": "Revocar",
"ethereum__approve_revoke_from": "Revocar de",
"ethereum__approve_to": "Aprobar a",
"ethereum__approve_unlimited_template": "Aprobando una cantidad ilimitada de {0}",
"ethereum__contract": "Contrato:",
"ethereum__data_size_template": "Tamaño: {0} bytes",
"ethereum__gas_limit": "Límite de gas",
@ -817,7 +828,6 @@
"solana__stake_question": "¿Hacer stake de SOL?",
"solana__stake_withdrawal_warning": "El monedero actual no podrá retirar de este staking.",
"solana__stake_withdrawal_warning_title": "Dirección de la autoridad de retiro",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "La transacción contiene instrucciones desconocidas.",
"solana__transaction_requires_x_signers_template": "La transacción requiere {0} firmantes, lo que aumenta la comisión.",
"solana__unstake": "Retirar stake",
@ -968,6 +978,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Comprar",
"words__cancel_and_exit": "Cancelar y salir",
"words__chain": "Cadena",
"words__confirm": "Confirmar",
"words__confirm_fee": "Confirmar comisión",
"words__contains": "Contiene",
@ -1004,8 +1015,10 @@
"words__title_success": "Completado",
"words__title_summary": "Resumen",
"words__title_threshold": "Umbral",
"words__token": "Token",
"words__try_again": "Reintentar.",
"words__unknown": "Desconocido",
"words__unlimited": "Ilimitado",
"words__unlocked": "Desbloqueado",
"words__warning": "Advertencia",
"words__writable": "Modificable",

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Voter pour le mandataire",
"eos__voter": "Votant:",
"ethereum__amount_sent": "Montant envoyé:",
"ethereum__approve": "Approuver",
"ethereum__approve_amount_allowance": "Montant autorisé",
"ethereum__approve_chain_id": "Chaîne ID",
"ethereum__approve_intro_title": "Autorisation de jetons",
"ethereum__approve_intro": "Vérifiez les informations pour autoriser l'utilisation des jetons.",
"ethereum__approve_intro_title_revoke": "Révocation de l'autorisation",
"ethereum__approve_intro_revoke": "Vérifiez les informations pour révoquer l'autorisation des jetons.",
"ethereum__approve_revoke": "Révoquer",
"ethereum__approve_revoke_from": "Révoquer de",
"ethereum__approve_to": "Approuver pour",
"ethereum__approve_unlimited_template": "Autoriser un montant illimité de {0}",
"ethereum__contract": "Contrat:",
"ethereum__data_size_template": "Taille: {0} octets",
"ethereum__gas_limit": "Limite de gaz",
@ -812,7 +823,6 @@
"solana__stake_question": "Staker de l'SOL ?",
"solana__stake_withdrawal_warning": "Ce portefeuille ne pourra pas être utilisé pour retirer de ce staking.",
"solana__stake_withdrawal_warning_title": "Adresse de lautorité de retrait",
"solana__title_token": "Jeton",
"solana__transaction_contains_unknown_instructions": "La transaction contient des instructions inconnues.",
"solana__transaction_requires_x_signers_template": "La transaction nécessite {0} signataires, ce qui augmente les frais.",
"solana__unstake": "Unstake",
@ -963,6 +973,7 @@
"words__blockhash": "Hachage de bloc",
"words__buying": "Achat",
"words__cancel_and_exit": "Annuler et quitter",
"words__chain": "Chaîne",
"words__confirm": "Conf.",
"words__confirm_fee": "Conf. les frais",
"words__contains": "Contient",
@ -999,8 +1010,10 @@
"words__title_success": "Réussite",
"words__title_summary": "Résumé",
"words__title_threshold": "Seuil",
"words__token": "Jeton",
"words__try_again": "Réessayer.",
"words__unknown": "Inconnu",
"words__unlimited": "Illimité",
"words__unlocked": "Déverrouillé",
"words__warning": "Avertissement",
"words__writable": "Modifiable",

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Voto per delega",
"eos__voter": "Votante:",
"ethereum__amount_sent": "Importo inviato:",
"ethereum__approve": "Approva",
"ethereum__approve_amount_allowance": "Limite importo",
"ethereum__approve_chain_id": "ID chain",
"ethereum__approve_intro_title": "Approv. token",
"ethereum__approve_intro": "Rivedi dettagli e approva spesa token.",
"ethereum__approve_intro_title_revoke": "Revoca token",
"ethereum__approve_intro_revoke": "Rivedi dettagli e revoca approv. token.",
"ethereum__approve_revoke": "Revoca",
"ethereum__approve_revoke_from": "Revoca da",
"ethereum__approve_to": "Approva a",
"ethereum__approve_unlimited_template": "Approvando importo illimitato di {0}",
"ethereum__contract": "Contratto:",
"ethereum__data_size_template": "Dimensioni: {0} byte",
"ethereum__gas_limit": "Limite gas:",
@ -800,7 +811,6 @@
"solana__is_provided_via_lookup_table_template": "{0} viene fornito tramite una tabella di ricerca.",
"solana__lookup_table_address": "Indirizzo tabella di ricerca",
"solana__multiple_signers": "Più firmatari",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "La transazione contiene istruzioni sconosciute.",
"solana__transaction_requires_x_signers_template": "Poiché la transazione richiede {0} firmatari, la commissione è più elevata.",
"stellar__account_merge": "Unione conti",
@ -948,6 +958,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Acquisto",
"words__cancel_and_exit": "Annulla ed esci",
"words__chain": "Catena",
"words__confirm": "Conferma",
"words__confirm_fee": "Conferma commissione",
"words__contains": "Contiene",
@ -984,8 +995,10 @@
"words__title_success": "Operazione riuscita",
"words__title_summary": "Riepilogo",
"words__title_threshold": "Soglia",
"words__token": "Token",
"words__try_again": "Riprova.",
"words__unknown": "Sconosciuto",
"words__unlimited": "Illimitato",
"words__unlocked": "Sbloccato",
"words__warning": "Avviso",
"words__writable": "Scrivibile",

View File

@ -998,5 +998,19 @@
"996": "words__unlocked",
"997": "solana__max_fees_rent",
"998": "solana__max_rent_fee",
"999": "solana__transaction_fee"
"999": "solana__transaction_fee",
"1000": "ethereum__approve",
"1001": "ethereum__approve_amount_allowance",
"1002": "ethereum__approve_chain_id",
"1003": "ethereum__approve_intro",
"1004": "ethereum__approve_intro_title",
"1005": "ethereum__approve_to",
"1006": "ethereum__approve_unlimited_template",
"1007": "words__unlimited",
"1008": "ethereum__approve_intro_revoke",
"1009": "ethereum__approve_intro_title_revoke",
"1010": "ethereum__approve_revoke",
"1011": "ethereum__approve_revoke_from",
"1012": "words__chain",
"1013": "words__token"
}

View File

@ -316,6 +316,17 @@
"eos__vote_for_proxy": "Votar para proxy",
"eos__voter": "Eleitor:",
"ethereum__amount_sent": "Quantia enviada:",
"ethereum__approve": "Aprovar",
"ethereum__approve_amount_allowance": "Limite quantia",
"ethereum__approve_chain_id": "ID chain",
"ethereum__approve_intro_title": "Aprov. token",
"ethereum__approve_intro": "Revise det. e aprove gasto de token.",
"ethereum__approve_intro_title_revoke": "Revog. token",
"ethereum__approve_intro_revoke": "Revise det. e revogue aprovação token.",
"ethereum__approve_revoke": "Revogar",
"ethereum__approve_revoke_from": "Revogar de",
"ethereum__approve_to": "Aprovar a",
"ethereum__approve_unlimited_template": "Aprovando quantia ilimitada de {0}",
"ethereum__contract": "Contrato:",
"ethereum__data_size_template": "Tamanho: {0} bytes",
"ethereum__gas_limit": "Limite de gás:",
@ -811,7 +822,6 @@
"solana__stake_question": "Fazer stake de SOL?",
"solana__stake_withdrawal_warning": "A carteira atual não é a autoridade de saque do staking de SOL.",
"solana__stake_withdrawal_warning_title": "Endereço da autoridade de saque",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "A transação contém instruções desconhecidas.",
"solana__transaction_requires_x_signers_template": "A transação exige {0} signatários, o que aumenta a taxa.",
"solana__unstake": "Tirar do stake",
@ -962,6 +972,7 @@
"words__blockhash": "Blockhash",
"words__buying": "Compra",
"words__cancel_and_exit": "Cancelar e sair",
"words__chain": "Cadeia",
"words__confirm": "Confirmar",
"words__confirm_fee": "Confirmar taxa",
"words__contains": "Contém",
@ -998,8 +1009,10 @@
"words__title_success": "Sucesso",
"words__title_summary": "Resumo",
"words__title_threshold": "Limite",
"words__token": "Token",
"words__try_again": "Tentar novamente.",
"words__unknown": "Desconhecido",
"words__unlimited": "Ilimitado",
"words__unlocked": "Desbloqueado",
"words__warning": "Aviso",
"words__writable": "Gravável",

View File

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "27c0e5fb8f144289e4e726678a0dfc3371d344ff5a7cca2233b1a4a009149653",
"datetime": "2025-05-07T13:50:09.500899",
"commit": "10cdd67b3e363d3e002d4736cdcb3e25ce41f186"
"merkle_root": "b0b4e9158f15d387ed0091716fca17b9523500faff4157b54084991c8db7a868",
"datetime": "2025-05-20T12:48:17.683102",
"commit": "51edc03279e8bef1ad676e2f998309ee4e190fcb"
},
"history": [
{

View File

@ -724,7 +724,6 @@
"solana__is_provided_via_lookup_table_template": "{0}, bir arama tablosu aracılığıyla sağlanır.",
"solana__lookup_table_address": "Arama tablosu adresi",
"solana__multiple_signers": "Birden fazla imzalayan",
"solana__title_token": "Token",
"solana__transaction_contains_unknown_instructions": "İşlem bilinmeyen talimatlar içeriyor.",
"solana__transaction_requires_x_signers_template": "İşlem için {0} imzalayan gerekiyor ve bu da ücreti artırıyor.",
"stellar__account_merge": "Hesap Birleştirme",
@ -897,6 +896,7 @@
"words__title_success": "Başari",
"words__title_summary": "Özet",
"words__title_threshold": "Eşi̇k",
"words__token": "Token",
"words__unknown": "Bilinmeyen",
"words__unlocked": "Kilidi Açık",
"words__warning": "Uyarı",

View File

@ -44,16 +44,23 @@ def make_defs(parameters: dict) -> messages.EthereumDefinitions:
# With removal of most built-in defs from firmware, we have test vectors
# that no longer run. Because this is not the place to test the definitions,
# we generate fake entries so that we can check the signing results.
address_n = parse_path(parameters["path"])
slip44 = unharden(address_n[1])
network = encode_eth_network(chain_id=parameters["chain_id"], slip44=slip44)
# However, we have the option to not generate the fake definitions,
# in case what we want to test is signing a tx for an unknown chain
# (which should be, well... not defined)!
if parameters.get("fake_defs", True):
address_n = parse_path(parameters["path"])
slip44 = unharden(address_n[1])
network = encode_eth_network(chain_id=parameters["chain_id"], slip44=slip44)
return messages.EthereumDefinitions(encoded_network=network)
return messages.EthereumDefinitions(encoded_network=network)
else:
return messages.EthereumDefinitions()
@parametrize_using_common_fixtures(
"ethereum/sign_tx.json",
"ethereum/sign_tx_eip155.json",
"ethereum/sign_tx_erc20.json",
)
@pytest.mark.parametrize("chunkify", (True, False))
def test_signtx(client: Client, chunkify: bool, parameters: dict, result: dict):