From 0ba8173424f14f7bbfba2e2fd2bd33510cd4502b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20Biz=C4=83u?= Date: Fri, 25 Apr 2025 08:45:20 +0200 Subject: [PATCH] feat(core): introduce a flow for ethereum approve --- .../fixtures/ethereum/sign_tx_erc20.json | 199 ++++++++++++++++++ core/.changelog.d/4542.added | 1 + core/embed/rust/librust_qstr.h | 15 +- .../generated/translated_string.rs | 81 ++++++- .../rust/src/ui/api/firmware_micropython.rs | 20 +- .../rust/src/ui/layout_bolt/ui_firmware.rs | 22 +- .../rust/src/ui/layout_caesar/ui_firmware.rs | 36 +++- .../rust/src/ui/layout_delizia/ui_firmware.rs | 15 +- core/embed/rust/src/ui/ui_firmware.rs | 7 +- core/embed/upymod/qstrdefsport.h | 4 +- core/mocks/generated/trezorui_api.pyi | 5 +- core/mocks/trezortranslate_keys.pyi | 15 +- core/src/apps/ethereum/layout.py | 80 +++++-- ...taking_tx_constants.py => sc_constants.py} | 18 +- core/src/apps/ethereum/sign_tx.py | 132 +++++++++--- core/src/apps/solana/layout.py | 2 +- core/src/trezor/ui/layouts/bolt/__init__.py | 113 +++++++++- core/src/trezor/ui/layouts/caesar/__init__.py | 117 +++++++++- .../src/trezor/ui/layouts/delizia/__init__.py | 107 +++++++++- core/translations/cs.json | 15 +- core/translations/de.json | 15 +- core/translations/en.json | 15 +- core/translations/es.json | 15 +- core/translations/fr.json | 15 +- core/translations/it.json | 15 +- core/translations/order.json | 16 +- core/translations/pt.json | 15 +- core/translations/signatures.json | 6 +- core/translations/tr.json | 2 +- tests/device_tests/ethereum/test_signtx.py | 15 +- 30 files changed, 1013 insertions(+), 120 deletions(-) create mode 100644 common/tests/fixtures/ethereum/sign_tx_erc20.json create mode 100644 core/.changelog.d/4542.added rename core/src/apps/ethereum/{staking_tx_constants.py => sc_constants.py} (57%) diff --git a/common/tests/fixtures/ethereum/sign_tx_erc20.json b/common/tests/fixtures/ethereum/sign_tx_erc20.json new file mode 100644 index 0000000000..2cd82513f0 --- /dev/null +++ b/common/tests/fixtures/ethereum/sign_tx_erc20.json @@ -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"] + } + ] +} diff --git a/core/.changelog.d/4542.added b/core/.changelog.d/4542.added new file mode 100644 index 0000000000..4eca121f16 --- /dev/null +++ b/core/.changelog.d/4542.added @@ -0,0 +1 @@ +Ethereum "approve" flow. diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index a3bb302af7..851e2fb3fe 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -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; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index dbf00b5491..62fc49f432 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -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), diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index f472aab533..f1bf12f639 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -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 = kwargs + .get(Qstr::MP_QSTR_amount) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let amount_label: Option = 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 = 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 = kwargs + .get(Qstr::MP_QSTR_account_title) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; let extra_items: Option = 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, diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs index b8db5feb1b..a8b01c2128 100644 --- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs @@ -427,23 +427,29 @@ impl FirmwareUI for UIBolt { } fn confirm_summary( - amount: TString<'static>, - amount_label: TString<'static>, + amount: Option>, + amount_label: Option>, fee: TString<'static>, fee_label: TString<'static>, title: Option>, account_items: Option, + _account_title: Option>, extra_items: Option, _extra_title: Option>, verb_cancel: Option>, ) -> Result { 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()? diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs index e843426697..3ae24b5756 100644 --- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs @@ -484,12 +484,13 @@ impl FirmwareUI for UICaesar { } fn confirm_summary( - amount: TString<'static>, - amount_label: TString<'static>, + amount: Option>, + amount_label: Option>, fee: TString<'static>, fee_label: TString<'static>, title: Option>, account_items: Option, + account_title: Option>, extra_items: Option, extra_title: Option>, verb_cancel: Option>, @@ -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( // 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 }; diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs index 7fe5836f62..d91ef9c4bd 100644 --- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs @@ -358,12 +358,13 @@ impl FirmwareUI for UIDelizia { } fn confirm_summary( - amount: TString<'static>, - amount_label: TString<'static>, + amount: Option>, + amount_label: Option>, fee: TString<'static>, fee_label: TString<'static>, title: Option>, account_items: Option, + account_title: Option>, extra_items: Option, extra_title: Option>, verb_cancel: Option>, @@ -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)); diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs index 438aa66b01..93c04c516f 100644 --- a/core/embed/rust/src/ui/ui_firmware.rs +++ b/core/embed/rust/src/ui/ui_firmware.rs @@ -130,13 +130,14 @@ pub trait FirmwareUI { #[allow(clippy::too_many_arguments)] fn confirm_summary( - amount: TString<'static>, - amount_label: TString<'static>, + amount: Option>, + amount_label: Option>, fee: TString<'static>, fee_label: TString<'static>, title: Option>, account_items: Option, // TODO: replace Obj - extra_items: Option, // TODO: replace Obj + account_title: Option>, + extra_items: Option, // TODO: replace Obj extra_title: Option>, verb_cancel: Option>, ) -> Result; diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h index 4387f37200..5a9e9d6ebf 100644 --- a/core/embed/upymod/qstrdefsport.h +++ b/core/embed/upymod/qstrdefsport.h @@ -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) diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index 5744772ca5..2c84a9a026 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -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, diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index e8aa0d0431..f39900c44f 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -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" diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index f4e97651fd..5a7cfd52c4 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -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, ) diff --git a/core/src/apps/ethereum/staking_tx_constants.py b/core/src/apps/ethereum/sc_constants.py similarity index 57% rename from core/src/apps/ethereum/staking_tx_constants.py rename to core/src/apps/ethereum/sc_constants.py index 89e6fcce07..59d249d65e 100644 --- a/core/src/apps/ethereum/staking_tx_constants.py +++ b/core/src/apps/ethereum/sc_constants.py @@ -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", +} diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index 8294b90b16..a9ede5514a 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -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: diff --git a/core/src/apps/solana/layout.py b/core/src/apps/solana/layout.py index 6d2e741136..c5fce3cd60 100644 --- a/core/src/apps/solana/layout.py +++ b/core/src/apps/solana/layout.py @@ -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", diff --git a/core/src/trezor/ui/layouts/bolt/__init__.py b/core/src/trezor/ui/layouts/bolt/__init__.py index 960c186f05..30123e425b 100644 --- a/core/src/trezor/ui/layouts/bolt/__init__.py +++ b/core/src/trezor/ui/layouts/bolt/__init__.py @@ -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, diff --git a/core/src/trezor/ui/layouts/caesar/__init__.py b/core/src/trezor/ui/layouts/caesar/__init__.py index a0ef604ad1..a5169cc4ae 100644 --- a/core/src/trezor/ui/layouts/caesar/__init__.py +++ b/core/src/trezor/ui/layouts/caesar/__init__.py @@ -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, diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py index 668a630b2d..e02738c6c5 100644 --- a/core/src/trezor/ui/layouts/delizia/__init__.py +++ b/core/src/trezor/ui/layouts/delizia/__init__.py @@ -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, diff --git a/core/translations/cs.json b/core/translations/cs.json index 639ef13d37..e6ba0c73ab 100644 --- a/core/translations/cs.json +++ b/core/translations/cs.json @@ -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é", diff --git a/core/translations/de.json b/core/translations/de.json index 2c357d9712..adf9207d65 100644 --- a/core/translations/de.json +++ b/core/translations/de.json @@ -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", diff --git a/core/translations/en.json b/core/translations/en.json index 5d1ee3ad6b..5f289e04c0 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -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", diff --git a/core/translations/es.json b/core/translations/es.json index 3e014b8366..437492cd13 100644 --- a/core/translations/es.json +++ b/core/translations/es.json @@ -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", diff --git a/core/translations/fr.json b/core/translations/fr.json index b9012ff253..8cb3dae1b4 100644 --- a/core/translations/fr.json +++ b/core/translations/fr.json @@ -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 l’autorité 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", diff --git a/core/translations/it.json b/core/translations/it.json index cf3b058e35..0b8219e77f 100644 --- a/core/translations/it.json +++ b/core/translations/it.json @@ -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", diff --git a/core/translations/order.json b/core/translations/order.json index 0825c853dc..1ebd729235 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -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" } diff --git a/core/translations/pt.json b/core/translations/pt.json index 6ca9f1fa94..135a79b6a8 100644 --- a/core/translations/pt.json +++ b/core/translations/pt.json @@ -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", diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 64ee992a99..59e0050f0c 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -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": [ { diff --git a/core/translations/tr.json b/core/translations/tr.json index 0b39601419..ea0fcd6d14 100644 --- a/core/translations/tr.json +++ b/core/translations/tr.json @@ -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ı", diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py index da1892d686..59519aa4ff 100644 --- a/tests/device_tests/ethereum/test_signtx.py +++ b/tests/device_tests/ethereum/test_signtx.py @@ -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):