From ebcf3e2db2b017ef5f83248379e9f33bb91651b5 Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sun, 11 Feb 2024 18:30:20 +0100 Subject: [PATCH] feat(core): confirm ETH stake, unstake, claim --- .../fixtures/ethereum/sign_tx_staking.json | 128 +++++++++++ .../ethereum/sign_tx_staking_data_error.json | 108 +++++++++ .../ethereum/sign_tx_staking_eip1559.json | 128 +++++++++++ core/.changelog.d/3517.added | 1 + core/embed/rust/librust_qstr.h | 9 + .../generated/translated_string.rs | 24 ++ core/embed/rust/src/ui/model_tt/layout.rs | 25 ++- core/mocks/generated/trezorui2.pyi | 2 + core/mocks/trezortranslate_keys.pyi | 8 + core/src/all_modules.py | 2 + core/src/apps/ethereum/helpers.py | 73 +++++- core/src/apps/ethereum/layout.py | 137 ++++++----- core/src/apps/ethereum/sign_tx.py | 212 ++++++++++++++++-- core/src/apps/ethereum/sign_tx_eip1559.py | 32 ++- .../src/apps/ethereum/staking_tx_constants.py | 21 ++ core/src/trezor/ui/layouts/tr/__init__.py | 52 ++++- core/src/trezor/ui/layouts/tt/__init__.py | 64 +++++- core/tests/test_apps.ethereum.layout.py | 3 +- core/translations/en.json | 8 + core/translations/order.json | 10 +- core/translations/signatures.json | 6 +- tests/device_tests/ethereum/test_signtx.py | 71 ++++++ tests/ui_tests/fixtures.json | 34 +++ tools/check-bitcoin-only | 2 +- vendor/fido2-tests | 2 +- 25 files changed, 1038 insertions(+), 124 deletions(-) create mode 100644 common/tests/fixtures/ethereum/sign_tx_staking.json create mode 100644 common/tests/fixtures/ethereum/sign_tx_staking_data_error.json create mode 100644 common/tests/fixtures/ethereum/sign_tx_staking_eip1559.json create mode 100644 core/.changelog.d/3517.added create mode 100644 core/src/apps/ethereum/staking_tx_constants.py diff --git a/common/tests/fixtures/ethereum/sign_tx_staking.json b/common/tests/fixtures/ethereum/sign_tx_staking.json new file mode 100644 index 000000000..0b8037c69 --- /dev/null +++ b/common/tests/fixtures/ethereum/sign_tx_staking.json @@ -0,0 +1,128 @@ +{ + "setup": { + "mnemonic": "alcohol woman abuse must during monitor noble actual mixed trade anger aisle", + "passphrase": "" + }, + "tests": [ + { + "name": "stake_holesky", + "parameters": { + "comment": "Stake transaction - Holesky testnet", + "data": "3a29dbae0000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 37, + "sig_r": "1a43606f1a3e9a61c4986cc9b324dd74f84557942cddf2e4ffccea82c2c54824", + "sig_s": "63e8595bfa1e383a1fea7205bff5afcf3b3d3513b45a2f792396f2f4251f4c55" + } + }, + { + "name": "stake_main", + "parameters": { + "comment": "Stake transaction - Mainnet", + "data": "3a29dbae0000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xD523794C879D9eC028960a231F866758e405bE34", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 37, + "sig_r": "104ae56ff2ec396a86191fa94b6e79af20efcc28e5d16b39d90fe05c990a2ce6", + "sig_s": "504fd80e50890df83a9b004d0ba97db11b93354327e717df5c0874036f616d47" + } + }, + { + "name": "unstake_holesky", + "parameters": { + "comment": "Unstake transaction - Holesky testnet", + "data": "76ec871c000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 38, + "sig_r": "f6553486737da2ceb42067047d0e8dd0add8e82f49b524cf657215e1d2487d16", + "sig_s": "21336e4c53537bcdf06a71ed6cbefc3374461476d8da6e988e6cf4957b6a8bb1" + } + }, + { + "name": "unstake_main", + "parameters": { + "comment": "Unstake transaction - Mainnet", + "data": "76ec871c000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xD523794C879D9eC028960a231F866758e405bE34", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 38, + "sig_r": "09077008477f40468928a94c45bfdb0b0ff473473401cd918740af4c98734bea", + "sig_s": "3cad467c41810a2d8901020a803d6836d1d5a39d8198eaba3eec121a48997b18" + } + }, + { + "name": "claim_holesky", + "parameters": { + "comment": "Claim transaction - Holesky testnet", + "data": "33986ffa", + "path": "m/44'/60'/0'/0/0", + "to_address": "0x624087DD1904ab122A32878Ce9e933C7071F53B9", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 37, + "sig_r": "4a77a5f7437c4744c8dc4e48f968a6fedccd86db0ee15ce6a832e71b17f11a9a", + "sig_s": "75e8efc0ecf1484ce745bbcd19899fbc458677869f3008be5ddb9c8ad8766d40" + } + }, + { + "name": "claim_mainnet", + "parameters": { + "comment": "Claim transaction - Mainnet", + "data": "33986ffa", + "path": "m/44'/60'/0'/0/0", + "to_address": "0x7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 37, + "sig_r": "c477417e2471d94c089d082fe1570fa8ba114e6572211c4b539244e2d40457d1", + "sig_s": "0bb029ed5dbe76016f74263c0a8709bcbd89affac62504196a1cf546d7c723a3" + } + } + ] +} diff --git a/common/tests/fixtures/ethereum/sign_tx_staking_data_error.json b/common/tests/fixtures/ethereum/sign_tx_staking_data_error.json new file mode 100644 index 000000000..40b5f9032 --- /dev/null +++ b/common/tests/fixtures/ethereum/sign_tx_staking_data_error.json @@ -0,0 +1,108 @@ +{ + "setup": { + "mnemonic": "alcohol woman abuse must during monitor noble actual mixed trade anger aisle", + "passphrase": "" + }, + "tests": [ + { + "name": "stake_bad_inputs_1", + "parameters": { + "comment": "Stake transaction - Holesky testnet. Wrong source argument (should be 1).", + "data": "3a29dbae0000000000000000000000000000000000000000000000000000000000000002", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 0, + "sig_r": "0x0", + "sig_s": "0x0" + } + }, + { + "name": "stake_bad_inputs_2", + "parameters": { + "comment": "Stake transaction - Mainnet. Missing arguments.", + "data": "3a29dbae", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xD523794C879D9eC028960a231F866758e405bE34", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 0, + "sig_r": "0x0", + "sig_s": "0x0" + } + }, + { + "name": "unstake_bad_inputs_1", + "parameters": { + "comment": "Unstake transaction - Holesky testnet. Wrong source argument (should be 1).", + "data": "76ec871c000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "0x0", + "sig_s": "0x0" + } + }, + { + "name": "unstake_bad_inputs_2", + "parameters": { + "comment": "Unstake transaction - Holesky testnet. Misaligned arguments.", + "data": "76ec871c000000000000000000", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "0x0", + "sig_s": "0x0" + } + }, + { + "name": "claim_bad_inputs_1", + "parameters": { + "comment": "Claim transaction - Mainnet. Misaligned data.", + "data": "33986ffaaa000aaa", + "path": "m/44'/60'/0'/0/0", + "to_address": "0x7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e", + "chain_id": 1, + "nonce": "0x0", + "gas_price": "0x14", + "gas_limit": "0x14", + "tx_type": null, + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "0x0", + "sig_s": "0x0" + } + } + ] +} diff --git a/common/tests/fixtures/ethereum/sign_tx_staking_eip1559.json b/common/tests/fixtures/ethereum/sign_tx_staking_eip1559.json new file mode 100644 index 000000000..9392e83c6 --- /dev/null +++ b/common/tests/fixtures/ethereum/sign_tx_staking_eip1559.json @@ -0,0 +1,128 @@ +{ + "setup": { + "mnemonic": "alcohol woman abuse must during monitor noble actual mixed trade anger aisle", + "passphrase": "" + }, + "tests": [ + { + "name": "stake_holesky", + "parameters": { + "comment": "Stake transaction - Holesky testnet", + "data": "3a29dbae0000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 0, + "sig_r": "77d68a17e2bcacccb791ca3d7e298588be0511a7d3c055abbb2030f54b56c6fa", + "sig_s": "567092e781fe1aedc458bdbc9ce4328398bf1d7d635787191881131c1afa7143" + } + }, + { + "name": "stake_main", + "parameters": { + "comment": "Stake transaction - Mainnet", + "data": "3a29dbae0000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xD523794C879D9eC028960a231F866758e405bE34", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x16345785D8A0000" + }, + "result": { + "sig_v": 0, + "sig_r": "05a37bf477df7f256729ec8607aa20a6286c42a246b5deebaf6d54915c0f87e3", + "sig_s": "30ffe2e80119452403bbf238b02def90a120ade2e73ab60926060ed10562f5ed" + } + }, + { + "name": "unstake_holesky", + "parameters": { + "comment": "Unstake transaction - Holesky testnet", + "data": "76ec871c000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xAFA848357154a6a624686b348303EF9a13F63264", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "9dfa73ea497785ffcb61598c554a57e46fce0a605c9ed06a4c8f265fccfd912e", + "sig_s": "45db0bd2189d4bc4828c859d28b907b3d47c4a35eef707052cde2f4fff4ef4fe" + } + }, + { + "name": "unstake_main", + "parameters": { + "comment": "Unstake transaction - Mainnet", + "data": "76ec871c000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "path": "m/44'/60'/0'/0/0", + "to_address": "0xD523794C879D9eC028960a231F866758e405bE34", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "15eb458e473c7f74abf99ba8833885e25435f913e77e5bc1259396942f6b9539", + "sig_s": "14ea7a1417cfab7bb2cdbd0750aa7929f02a30999b624dd2ae1029c6be9dc9d8" + } + }, + { + "name": "claim_holesky", + "parameters": { + "comment": "Claim transaction - Holesky testnet", + "data": "33986ffa", + "path": "m/44'/60'/0'/0/0", + "to_address": "0x624087DD1904ab122A32878Ce9e933C7071F53B9", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x0" + }, + "result": { + "sig_v": 1, + "sig_r": "5abf2a99ac6431fce9e436234e2962502b1a67d4b582f90f4bc592e790f8fd7e", + "sig_s": "60b01cd76d40f089dc9464cb442c6bbac435f80a35b5a1ed128de7cb9718aa3a" + } + }, + { + "name": "claim_mainnet", + "parameters": { + "comment": "Claim transaction - Mainnet", + "data": "33986ffa", + "path": "m/44'/60'/0'/0/0", + "to_address": "0x7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e", + "chain_id": 1, + "nonce": "0x0", + "gas_limit": "0x14", + "max_gas_fee": "0x14", + "max_priority_fee": "0x1", + "value": "0x0" + }, + "result": { + "sig_v": 0, + "sig_r": "7b3a09f1e0b28b2a25ddc3de02168fc20e9faada1d97181679dfb6f8383382da", + "sig_s": "6667dbb7b7b63d3e51df012a62dc6b01f13b60c7598b04aa499722cfb26864f3" + } + } + ] +} diff --git a/core/.changelog.d/3517.added b/core/.changelog.d/3517.added new file mode 100644 index 000000000..c5ae5d78c --- /dev/null +++ b/core/.changelog.d/3517.added @@ -0,0 +1 @@ +Clear sign ETH staking transactions on Everstake pool. diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 3ec16abea..bfa11f0d5 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -367,6 +367,14 @@ static void _librust_qstrs(void) { MP_QSTR_ethereum__show_full_message; MP_QSTR_ethereum__show_full_struct; MP_QSTR_ethereum__sign_eip712; + MP_QSTR_ethereum__staking_claim; + MP_QSTR_ethereum__staking_claim_address; + MP_QSTR_ethereum__staking_claim_intro; + MP_QSTR_ethereum__staking_stake; + MP_QSTR_ethereum__staking_stake_address; + MP_QSTR_ethereum__staking_stake_intro; + MP_QSTR_ethereum__staking_unstake; + MP_QSTR_ethereum__staking_unstake_intro; MP_QSTR_ethereum__title_confirm_data; MP_QSTR_ethereum__title_confirm_domain; MP_QSTR_ethereum__title_confirm_message; @@ -898,6 +906,7 @@ static void _librust_qstrs(void) { MP_QSTR_stellar__your_account; MP_QSTR_subprompt; MP_QSTR_subtitle; + MP_QSTR_text_mono; MP_QSTR_tezos__baker_address; MP_QSTR_tezos__balance; MP_QSTR_tezos__ballot; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 257eb84f4..b3fa2d9be 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -844,6 +844,14 @@ pub enum TranslatedString { words__yes = 831, reboot_to_bootloader__just_a_moment = 832, inputs__previous = 833, + ethereum__staking_claim = 834, + ethereum__staking_claim_address = 835, + ethereum__staking_claim_intro = 836, + ethereum__staking_stake = 837, + ethereum__staking_stake_address = 838, + ethereum__staking_stake_intro = 839, + ethereum__staking_unstake = 840, + ethereum__staking_unstake_intro = 841, } impl TranslatedString { @@ -1683,6 +1691,14 @@ impl TranslatedString { Self::words__yes => "Yes", Self::reboot_to_bootloader__just_a_moment => "Just a moment...", Self::inputs__previous => "PREVIOUS", + Self::ethereum__staking_claim => "CLAIM", + Self::ethereum__staking_claim_address => "CLAIM ADDRESS", + Self::ethereum__staking_claim_intro => "Claim ETH from Everstake?", + Self::ethereum__staking_stake => "STAKE", + Self::ethereum__staking_stake_address => "STAKE ADDRESS", + Self::ethereum__staking_stake_intro => "Stake ETH on Everstake?", + Self::ethereum__staking_unstake => "UNSTAKE", + Self::ethereum__staking_unstake_intro => "Unstake ETH from Everstake?", } } @@ -2523,6 +2539,14 @@ impl TranslatedString { Qstr::MP_QSTR_words__yes => Some(Self::words__yes), Qstr::MP_QSTR_reboot_to_bootloader__just_a_moment => Some(Self::reboot_to_bootloader__just_a_moment), Qstr::MP_QSTR_inputs__previous => Some(Self::inputs__previous), + Qstr::MP_QSTR_ethereum__staking_claim => Some(Self::ethereum__staking_claim), + Qstr::MP_QSTR_ethereum__staking_claim_address => Some(Self::ethereum__staking_claim_address), + Qstr::MP_QSTR_ethereum__staking_claim_intro => Some(Self::ethereum__staking_claim_intro), + Qstr::MP_QSTR_ethereum__staking_stake => Some(Self::ethereum__staking_stake), + Qstr::MP_QSTR_ethereum__staking_stake_address => Some(Self::ethereum__staking_stake_address), + Qstr::MP_QSTR_ethereum__staking_stake_intro => Some(Self::ethereum__staking_stake_intro), + Qstr::MP_QSTR_ethereum__staking_unstake => Some(Self::ethereum__staking_unstake), + Qstr::MP_QSTR_ethereum__staking_unstake_intro => Some(Self::ethereum__staking_unstake_intro), _ => None, } } diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index c8264bdd8..3616532d7 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -463,6 +463,7 @@ struct ConfirmBlobParams { info_button: bool, hold: bool, chunkify: bool, + text_mono: bool, } impl ConfirmBlobParams { @@ -485,6 +486,7 @@ impl ConfirmBlobParams { info_button: false, hold, chunkify: false, + text_mono: true, } } @@ -508,6 +510,11 @@ impl ConfirmBlobParams { self } + fn with_text_mono(mut self, text_mono: bool) -> Self { + self.text_mono = text_mono; + self + } + fn into_layout(self) -> Result { let paragraphs = ConfirmBlob { description: self.description.unwrap_or_else(StrBuffer::empty), @@ -518,8 +525,10 @@ impl ConfirmBlobParams { data_font: if self.chunkify { let data: StrBuffer = self.data.try_into()?; theme::get_chunkified_text_style(data.len()) - } else { + } else if self.text_mono { &theme::TEXT_MONO + } else { + &theme::TEXT_NORMAL }, } .into_paragraphs(); @@ -738,6 +747,7 @@ extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs: let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; let horizontal: bool = kwargs.get_or(Qstr::MP_QSTR_horizontal, false)?; + let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; let mut paragraphs = ParagraphVecShort::new(); @@ -746,7 +756,14 @@ extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs: let key: StrBuffer = key.try_into()?; let value: StrBuffer = value.try_into()?; paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, key).no_break()); - paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value)); + if chunkify { + paragraphs.add(Paragraph::new( + theme::get_chunkified_text_style(value.len()), + value, + )); + } else { + paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value)); + } } let axis = match horizontal { @@ -787,11 +804,13 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma .try_into_option()?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?; + let text_mono: bool = kwargs.get_or(Qstr::MP_QSTR_text_mono, true)?; ConfirmBlobParams::new(title, value, description, verb, verb_cancel, hold) .with_subtitle(subtitle) .with_info_button(info_button) .with_chunkify(chunkify) + .with_text_mono(text_mono) .into_layout() }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -1779,6 +1798,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// items: Iterable[Tuple[str, str]], /// horizontal: bool = False, + /// chunkify: bool = False, /// ) -> object: /// """Show metadata for outgoing transaction.""" Qstr::MP_QSTR_show_info_with_cancel => obj_fn_kw!(0, new_show_info_with_cancel).as_obj(), @@ -1794,6 +1814,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// info_button: bool = False, /// hold: bool = False, /// chunkify: bool = False, + /// text_mono: bool = True, /// ) -> object: /// """Confirm value. Merge of confirm_total and confirm_output.""" Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(), diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 96a2e6608..fa7d04aad 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -574,6 +574,7 @@ def show_info_with_cancel( title: str, items: Iterable[Tuple[str, str]], horizontal: bool = False, + chunkify: bool = False, ) -> object: """Show metadata for outgoing transaction.""" @@ -590,6 +591,7 @@ def confirm_value( info_button: bool = False, hold: bool = False, chunkify: bool = False, + text_mono: bool = True, ) -> object: """Confirm value. Merge of confirm_total and confirm_output.""" diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 8dce4f45f..de70ad804 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -288,6 +288,14 @@ class TR: ethereum__show_full_message: str = "Show full message" ethereum__show_full_struct: str = "Show full struct" ethereum__sign_eip712: str = "Really sign EIP-712 typed data?" + ethereum__staking_claim: str = "CLAIM" + ethereum__staking_claim_address: str = "CLAIM ADDRESS" + ethereum__staking_claim_intro: str = "Claim ETH from Everstake?" + ethereum__staking_stake: str = "STAKE" + ethereum__staking_stake_address: str = "STAKE ADDRESS" + ethereum__staking_stake_intro: str = "Stake ETH on Everstake?" + ethereum__staking_unstake: str = "UNSTAKE" + ethereum__staking_unstake_intro: str = "Unstake ETH from Everstake?" ethereum__title_confirm_data: str = "CONFIRM DATA" ethereum__title_confirm_domain: str = "CONFIRM DOMAIN" ethereum__title_confirm_message: str = "CONFIRM MESSAGE" diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 89d54c72c..e1162a3c2 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -535,6 +535,8 @@ if not utils.BITCOIN_ONLY: import apps.ethereum.sign_tx_eip1559 apps.ethereum.sign_typed_data import apps.ethereum.sign_typed_data + apps.ethereum.staking_tx_constants + import apps.ethereum.staking_tx_constants apps.ethereum.tokens import apps.ethereum.tokens apps.ethereum.verify_message diff --git a/core/src/apps/ethereum/helpers.py b/core/src/apps/ethereum/helpers.py index ad3c34a74..7ce265d1a 100644 --- a/core/src/apps/ethereum/helpers.py +++ b/core/src/apps/ethereum/helpers.py @@ -1,13 +1,16 @@ from typing import TYPE_CHECKING from ubinascii import hexlify +from trezor import TR + from . import networks if TYPE_CHECKING: - from trezor.messages import EthereumFieldType + from typing import Iterable - from .networks import EthereumNetworkInfo + from trezor.messages import EthereumFieldType, EthereumTokenInfo + from .networks import EthereumNetworkInfo RSKIP60_NETWORKS = (30, 31) @@ -126,6 +129,72 @@ def decode_typed_data(data: bytes, type_name: str) -> str: raise ValueError # Unsupported data type for direct field decoding +def get_fee_items_regular( + gas_price: int, gas_limit: int, network: EthereumNetworkInfo +) -> Iterable[tuple[str, str]]: + # regular + gas_limit_str = TR.ethereum__units_template.format(gas_limit) + gas_price_str = format_ethereum_amount( + gas_price, None, network, force_unit_gwei=True + ) + + return ( + (TR.ethereum__gas_limit, gas_limit_str), + (TR.ethereum__gas_price, gas_price_str), + ) + + +def get_fee_items_eip1559( + max_gas_fee: int, + max_priority_fee: int, + gas_limit: int, + network: EthereumNetworkInfo, +) -> Iterable[tuple[str, str]]: + # EIP-1559 + gas_limit_str = TR.ethereum__units_template.format(gas_limit) + max_gas_fee_str = format_ethereum_amount( + max_gas_fee, None, network, force_unit_gwei=True + ) + max_priority_fee_str = format_ethereum_amount( + max_priority_fee, None, network, force_unit_gwei=True + ) + + return ( + (TR.ethereum__gas_limit, gas_limit_str), + (TR.ethereum__max_gas_price, max_gas_fee_str), + (TR.ethereum__priority_fee, max_priority_fee_str), + ) + + +def format_ethereum_amount( + value: int, + token: EthereumTokenInfo | None, + network: EthereumNetworkInfo, + force_unit_gwei: bool = False, +) -> str: + from trezor.strings import format_amount + + if token: + suffix = token.symbol + decimals = token.decimals + else: + suffix = network.symbol + decimals = 18 + + if force_unit_gwei: + assert token is None + assert decimals >= 9 + decimals = decimals - 9 + suffix = "Gwei" + elif decimals > 9 and value < 10 ** (decimals - 9): + # Don't want to display wei values for tokens with small decimal numbers + suffix = "Wei " + suffix + decimals = 0 + + amount = format_amount(value, decimals) + return f"{amount} {suffix}" + + def _from_bytes_bigendian_signed(b: bytes) -> int: negative = b[0] & 0x80 if negative: diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 4740d2146..20bd76cb7 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -4,12 +4,12 @@ from trezor import TR, ui from trezor.enums import ButtonRequestType from trezor.ui.layouts import ( confirm_blob, - confirm_ethereum_tx, + confirm_ethereum_staking_tx, confirm_text, should_show_more, ) -from .helpers import address_from_bytes, decode_typed_data +from .helpers import address_from_bytes, decode_typed_data, format_ethereum_amount if TYPE_CHECKING: from typing import Awaitable, Iterable @@ -25,12 +25,14 @@ if TYPE_CHECKING: async def require_confirm_tx( to_bytes: bytes, value: int, - gas_price: int, - gas_limit: int, + maximum_fee: str, + fee_info_items: Iterable[tuple[str, str]], network: EthereumNetworkInfo, token: EthereumTokenInfo | None, chunkify: bool, ) -> None: + from trezor.ui.layouts import confirm_ethereum_tx + if to_bytes: to_str = address_from_bytes(to_bytes, network) else: @@ -38,56 +40,80 @@ async def require_confirm_tx( chunkify = False total_amount = format_ethereum_amount(value, token, network) - maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, network) - gas_limit_str = TR.ethereum__units_template.format(gas_limit) - gas_price_str = format_ethereum_amount( - gas_price, None, network, force_unit_gwei=True - ) - - items = ( - (TR.ethereum__gas_limit, gas_limit_str), - (TR.ethereum__gas_price, gas_price_str), - ) await confirm_ethereum_tx( - to_str, total_amount, maximum_fee, items, chunkify=chunkify + to_str, total_amount, maximum_fee, fee_info_items, chunkify=chunkify ) -async def require_confirm_tx_eip1559( - to_bytes: bytes, +async def require_confirm_stake( + addr_bytes: bytes, value: int, - max_gas_fee: int, - max_priority_fee: int, - gas_limit: int, + maximum_fee: str, + fee_info_items: Iterable[tuple[str, str]], network: EthereumNetworkInfo, - token: EthereumTokenInfo | None, chunkify: bool, ) -> None: - if to_bytes: - to_str = address_from_bytes(to_bytes, network) - else: - to_str = TR.ethereum__new_contract - chunkify = False - total_amount = format_ethereum_amount(value, token, network) - maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, network) - gas_limit_str = TR.ethereum__units_template.format(gas_limit) - max_gas_fee_str = format_ethereum_amount( - max_gas_fee, None, network, force_unit_gwei=True - ) - max_priority_fee_str = format_ethereum_amount( - max_priority_fee, None, network, force_unit_gwei=True + addr_str = address_from_bytes(addr_bytes, network) + total_amount = format_ethereum_amount(value, None, network) + await confirm_ethereum_staking_tx( + TR.ethereum__staking_stake, # title + TR.ethereum__staking_stake_intro, # intro_question + TR.ethereum__staking_stake, # verb + total_amount, # total_amount + maximum_fee, # maximum_fee + addr_str, # address + TR.ethereum__staking_stake_address, # address_title + fee_info_items, # info_items + chunkify=chunkify, ) - items: tuple[tuple[str, str], ...] = ( - (TR.ethereum__gas_limit, gas_limit_str), - (TR.ethereum__max_gas_price, max_gas_fee_str), - (TR.ethereum__priority_fee, max_priority_fee_str), + +async def require_confirm_unstake( + addr_bytes: bytes, + value: int, + maximum_fee: str, + fee_info_items: Iterable[tuple[str, str]], + network: EthereumNetworkInfo, + chunkify: bool, +) -> None: + + addr_str = address_from_bytes(addr_bytes, network) + total_amount = format_ethereum_amount(value, None, network) + + await confirm_ethereum_staking_tx( + TR.ethereum__staking_unstake, # title + TR.ethereum__staking_unstake_intro, # intro_question + TR.ethereum__staking_unstake, # verb + total_amount, # total_amount + maximum_fee, # maximum_fee + addr_str, # address + TR.ethereum__staking_stake_address, # address_title + fee_info_items, # info_items + chunkify=chunkify, ) - await confirm_ethereum_tx( - to_str, total_amount, maximum_fee, items, chunkify=chunkify + +async def require_confirm_claim( + addr_bytes: bytes, + maximum_fee: str, + fee_info_items: Iterable[tuple[str, str]], + network: EthereumNetworkInfo, + chunkify: bool, +) -> None: + + addr_str = address_from_bytes(addr_bytes, network) + await confirm_ethereum_staking_tx( + TR.ethereum__staking_claim, # title + TR.ethereum__staking_claim_intro, # intro_question + TR.ethereum__staking_claim, # verb + "", # total_amount + maximum_fee, # maximum_fee + addr_str, # address + TR.ethereum__staking_claim_address, # address_title + fee_info_items, # info_items + chunkify=chunkify, ) @@ -119,7 +145,7 @@ def require_confirm_address(address_bytes: bytes) -> Awaitable[None]: ) -def require_confirm_data(data: bytes, data_total: int) -> Awaitable[None]: +def require_confirm_other_data(data: bytes, data_total: int) -> Awaitable[None]: return confirm_blob( "confirm_data", TR.ethereum__title_confirm_data, @@ -258,35 +284,6 @@ async def confirm_typed_value( ) -def format_ethereum_amount( - value: int, - token: EthereumTokenInfo | None, - network: EthereumNetworkInfo, - force_unit_gwei: bool = False, -) -> str: - from trezor.strings import format_amount - - if token: - suffix = token.symbol - decimals = token.decimals - else: - suffix = network.symbol - decimals = 18 - - if force_unit_gwei: - assert token is None - assert decimals >= 9 - decimals = decimals - 9 - suffix = "Gwei" - elif decimals > 9 and value < 10 ** (decimals - 9): - # Don't want to display wei values for tokens with small decimal numbers - suffix = "Wei " + suffix - decimals = 0 - - amount = format_amount(value, decimals) - return f"{amount} {suffix}" - - def limit_str(s: str, limit: int = 16) -> str: """Shortens string to show the last characters.""" if len(s) <= limit + 2: diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index f843e7ef0..37af6424f 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -2,13 +2,23 @@ from typing import TYPE_CHECKING from trezor.crypto import rlp 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 .helpers import bytes_from_address from .keychain import with_keychain_from_chain_id if TYPE_CHECKING: - from trezor.messages import EthereumSignTx, EthereumTokenInfo, EthereumTxAck + from typing import Iterable + + from trezor.messages import ( + EthereumNetworkInfo, + EthereumSignTx, + EthereumTokenInfo, + EthereumTxAck, + ) from apps.common.keychain import Keychain @@ -33,7 +43,9 @@ async def sign_tx( from apps.common import paths - from .layout import require_confirm_data, require_confirm_tx + from .helpers import format_ethereum_amount, get_fee_items_regular + + data_total = msg.data_length # local_cache_attribute # check if msg.tx_type not in [1, 6, None]: @@ -42,26 +54,20 @@ async def sign_tx( raise DataError("Fee overflow") check_common_fields(msg) + # have a user confirm signing await paths.validate_path(keychain, msg.address_n) - - # Handle ERC20s - token, address_bytes, recipient, value = await handle_erc20(msg, defs) - - data_total = msg.data_length # local_cache_attribute - - if token is None and data_total > 0: - await require_confirm_data(msg.data_initial_chunk, data_total) - - await require_confirm_tx( - recipient, - value, - int.from_bytes(msg.gas_price, "big"), - int.from_bytes(msg.gas_limit, "big"), + address_bytes = bytes_from_address(msg.to) + gas_price = int.from_bytes(msg.gas_price, "big") + gas_limit = int.from_bytes(msg.gas_limit, "big") + maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, defs.network) + fee_items = get_fee_items_regular( + gas_price, + gas_limit, defs.network, - token, - bool(msg.chunkify), ) + await confirm_tx_data(msg, defs, address_bytes, maximum_fee, fee_items, data_total) + # sign data = bytearray() data += msg.data_initial_chunk data_left = data_total - len(msg.data_initial_chunk) @@ -99,16 +105,89 @@ async def sign_tx( return result -async def handle_erc20( +async def confirm_tx_data( + msg: MsgInSignTx, + defs: Definitions, + address_bytes: bytes, + maximum_fee: str, + fee_items: Iterable[tuple[str, str]], + data_total_len: int, +) -> None: + # function distinguishes between staking / smart contracts / regular transactions + from .layout import 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) + + if token is None and data_total_len > 0: + await require_confirm_other_data(msg.data_initial_chunk, data_total_len) + + await require_confirm_tx( + recipient, + value, + maximum_fee, + fee_items, + defs.network, + token, + bool(msg.chunkify), + ) + + +async def handle_staking( + msg: MsgInSignTx, + network: EthereumNetworkInfo, + address_bytes: bytes, + maximum_fee: str, + fee_items: Iterable[tuple[str, str]], +) -> bool: + + data_reader = BufferReader(msg.data_initial_chunk) + if data_reader.remaining_count() < constants.SC_FUNC_SIG_BYTES: + return False + + func_sig = data_reader.read_memoryview(constants.SC_FUNC_SIG_BYTES) + if address_bytes in constants.ADDRESSES_POOL: + if func_sig == constants.SC_FUNC_SIG_STAKE: + await _handle_staking_tx_stake( + data_reader, msg, network, address_bytes, maximum_fee, fee_items + ) + return True + if func_sig == constants.SC_FUNC_SIG_UNSTAKE: + await _handle_staking_tx_unstake( + data_reader, msg, network, address_bytes, maximum_fee, fee_items + ) + return True + + if address_bytes in constants.ADDRESSES_ACCOUNTING: + if func_sig == constants.SC_FUNC_SIG_CLAIM: + await _handle_staking_tx_claim( + data_reader, + address_bytes, + maximum_fee, + fee_items, + network, + bool(msg.chunkify), + ) + return True + + # data not corresponding to staking transaction + return False + + +async def handle_erc20_transfer( msg: MsgInSignTx, definitions: Definitions, -) -> tuple[EthereumTokenInfo | None, bytes, bytes, int]: + address_bytes: bytes, +) -> tuple[EthereumTokenInfo | None, bytes, int]: from . import tokens from .layout import require_confirm_unknown_token data_initial_chunk = msg.data_initial_chunk # local_cache_attribute token = None - address_bytes = recipient = bytes_from_address(msg.to) + recipient = address_bytes value = int.from_bytes(msg.value, "big") if ( len(msg.to) in (40, 42) @@ -125,7 +204,7 @@ async def handle_erc20( if token is tokens.UNKNOWN_TOKEN: await require_confirm_unknown_token(address_bytes) - return token, address_bytes, recipient, value + return token, recipient, value def _get_total_length(msg: EthereumSignTx, data_total: int) -> int: @@ -208,3 +287,92 @@ def check_common_fields(msg: MsgInSignTx) -> None: if msg.chain_id == 0: raise DataError("Chain ID out of bounds") + + +async def _handle_staking_tx_stake( + data_reader: BufferReader, + msg: MsgInSignTx, + network: EthereumNetworkInfo, + address_bytes: bytes, + maximum_fee: str, + fee_items: Iterable[tuple[str, str]], +) -> None: + from .layout import require_confirm_stake + + # stake args: + # - arg0: uint64, source (should be 1) + try: + source = int.from_bytes( + data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES), "big" + ) + if source != 1: + raise ValueError # wrong value of 1st argument ('source' should be 1) + if data_reader.remaining_count() != 0: + raise ValueError # wrong number of arguments for stake (should be 1) + except (ValueError, EOFError): + raise DataError("Invalid staking transaction call") + + await require_confirm_stake( + address_bytes, + int.from_bytes(msg.value, "big"), + maximum_fee, + fee_items, + network, + bool(msg.chunkify), + ) + + +async def _handle_staking_tx_unstake( + data_reader: BufferReader, + msg: MsgInSignTx, + network: EthereumNetworkInfo, + address_bytes: bytes, + maximum_fee: str, + fee_items: Iterable[tuple[str, str]], +) -> None: + from .layout import require_confirm_unstake + + # unstake args: + # - arg0: uint256, value + # - arg1: uint16, isAllowedInterchange (bool) + # - arg2: uint64, source, should be 1 + try: + value = int.from_bytes( + data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES), "big" + ) + _ = data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES) # skip arg1 + source = int.from_bytes( + data_reader.read_memoryview(constants.SC_ARGUMENT_BYTES), "big" + ) + if source != 1: + raise ValueError # wrong value of 3rd argument ('source' should be 1) + if data_reader.remaining_count() != 0: + raise ValueError # wrong number of arguments for unstake (should be 3) + except (ValueError, EOFError): + raise DataError("Invalid staking transaction call") + + await require_confirm_unstake( + address_bytes, + value, + maximum_fee, + fee_items, + network, + bool(msg.chunkify), + ) + + +async def _handle_staking_tx_claim( + data_reader: BufferReader, + staking_addr: bytes, + maximum_fee: str, + fee_items: Iterable[tuple[str, str]], + network: EthereumNetworkInfo, + chunkify: bool, +) -> None: + from .layout import require_confirm_claim + + # claim has no args + if data_reader.remaining_count() != 0: + raise DataError("Invalid staking transaction call") + + await require_confirm_claim(staking_addr, maximum_fee, fee_items, network, chunkify) diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py index d9954ad12..a4cb1d286 100644 --- a/core/src/apps/ethereum/sign_tx_eip1559.py +++ b/core/src/apps/ethereum/sign_tx_eip1559.py @@ -42,8 +42,8 @@ async def sign_tx_eip1559( from apps.common import paths - from .layout import require_confirm_data, require_confirm_tx_eip1559 - from .sign_tx import check_common_fields, handle_erc20, send_request_chunk + from .helpers import format_ethereum_amount, get_fee_items_eip1559 + from .sign_tx import check_common_fields, confirm_tx_data, send_request_chunk gas_limit = msg.gas_limit # local_cache_attribute data_total = msg.data_length # local_cache_attribute @@ -55,25 +55,23 @@ async def sign_tx_eip1559( raise wire.DataError("Fee overflow") check_common_fields(msg) + # have a user confirm signing await paths.validate_path(keychain, msg.address_n) - - # Handle ERC20s - token, address_bytes, recipient, value = await handle_erc20(msg, defs) - - if token is None and data_total > 0: - await require_confirm_data(msg.data_initial_chunk, data_total) - - await require_confirm_tx_eip1559( - recipient, - value, - int.from_bytes(msg.max_gas_fee, "big"), - int.from_bytes(msg.max_priority_fee, "big"), - int.from_bytes(gas_limit, "big"), + address_bytes = bytes_from_address(msg.to) + + max_gas_fee = int.from_bytes(msg.max_gas_fee, "big") + max_priority_fee = int.from_bytes(msg.max_priority_fee, "big") + gas_limit = int.from_bytes(msg.gas_limit, "big") + maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, defs.network) + fee_items = get_fee_items_eip1559( + max_gas_fee, + max_priority_fee, + gas_limit, defs.network, - token, - bool(msg.chunkify), ) + await confirm_tx_data(msg, defs, address_bytes, maximum_fee, fee_items, data_total) + # transaction data confirmed, proceed with signing data = bytearray() data += msg.data_initial_chunk data_left = data_total - len(msg.data_initial_chunk) diff --git a/core/src/apps/ethereum/staking_tx_constants.py b/core/src/apps/ethereum/staking_tx_constants.py new file mode 100644 index 000000000..89e6fcce0 --- /dev/null +++ b/core/src/apps/ethereum/staking_tx_constants.py @@ -0,0 +1,21 @@ +from micropython import const +from ubinascii import unhexlify + +# smart contract 'data' field lengths in bytes +SC_FUNC_SIG_BYTES = const(4) +SC_ARGUMENT_BYTES = const(32) + +# staking operations function signatures +SC_FUNC_SIG_STAKE = unhexlify("3a29dbae") +SC_FUNC_SIG_UNSTAKE = unhexlify("76ec871c") +SC_FUNC_SIG_CLAIM = unhexlify("33986ffa") + +# addresses for pool (stake/unstake) and accounting (claim) operations +ADDRESSES_POOL = ( + unhexlify("AFA848357154a6a624686b348303EF9a13F63264"), # holesky testnet + unhexlify("D523794C879D9eC028960a231F866758e405bE34"), # mainnet +) +ADDRESSES_ACCOUNTING = ( + unhexlify("624087DD1904ab122A32878Ce9e933C7071F53B9"), # holesky testnet + unhexlify("7a7f0b3c23C23a31cFcb0c44709be70d4D545c6e"), # mainnet +) diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index c42996683..ba5f3409d 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -1023,7 +1023,7 @@ async def confirm_value( title=info_title.upper(), action=info_value, description=description, - verb=TR.buttons__back, + verb="", verb_cancel="<", hold=False, reverse=False, @@ -1067,6 +1067,56 @@ async def confirm_total( ) +async def confirm_ethereum_staking_tx( + title: str, + intro_question: str, + verb: str, + total_amount: str, + maximum_fee: str, + address: str, + address_title: str, + info_items: Iterable[tuple[str, str]], + br_type: str = "confirm_ethereum_staking_tx", + br_code: ButtonRequestType = ButtonRequestType.SignTx, + chunkify: bool = False, +) -> None: + + # intro + await confirm_value( + title, + intro_question, + "", + br_type, + br_code, + verb=verb, + info_items=((address_title, address),), + ) + + # confirmation + if verb == TR.ethereum__staking_claim: + amount_title = verb + amount_value = "" + else: + amount_title = TR.words__amount + ":" + amount_value = total_amount + await raise_if_not_confirmed( + interact( + RustLayout( + trezorui2.altcoin_tx_summary( + amount_title=amount_title, + amount_value=amount_value, + fee_title=TR.send__maximum_fee, + fee_value=maximum_fee, + items=info_items, + cancel_cross=True, + ) + ), + br_type=br_type, + br_code=br_code, + ) + ) + + async def confirm_solana_tx( amount: str, fee: str, diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index 77535388b..e162b107b 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -859,6 +859,7 @@ def confirm_value( verb: str | None = None, subtitle: str | None = None, hold: bool = False, + value_text_mono: bool = True, info_items: Iterable[tuple[str, str]] | None = None, ) -> Awaitable[None]: """General confirmation dialog, used by many other confirm_* functions.""" @@ -948,7 +949,11 @@ async def confirm_total( info_items.append((TR.confirm_total__fee_rate, fee_rate_amount)) await confirm_summary( - items, TR.words__title_summary, info_items, br_type=br_type, br_code=br_code + items, + TR.words__title_summary, + info_items=info_items, + br_type=br_type, + br_code=br_code, ) @@ -956,6 +961,7 @@ async def confirm_summary( items: Iterable[tuple[str, str]], title: str | None = None, info_items: Iterable[tuple[str, str]] | None = None, + info_title: str | None = None, br_type: str = "confirm_total", br_code: ButtonRequestType = ButtonRequestType.SignTx, ) -> None: @@ -971,7 +977,7 @@ async def confirm_summary( info_items = info_items or [] info_layout = RustLayout( trezorui2.show_info_with_cancel( - title=TR.words__title_information, + title=info_title.upper() if info_title else TR.words__title_information, items=info_items, ) ) @@ -1025,6 +1031,60 @@ async def confirm_ethereum_tx( continue +async def confirm_ethereum_staking_tx( + title: str, + intro_question: str, + verb: str, + total_amount: str, + maximum_fee: str, + address: str, + address_title: str, + info_items: Iterable[tuple[str, str]] | None = None, + chunkify: bool = False, + br_type: str = "confirm_ethereum_staking_tx", + br_code: ButtonRequestType = ButtonRequestType.SignTx, +) -> None: + + # intro + # NOTE: this layout very similar to `confirm_value` with some adjustments + msg_layout = RustLayout( + trezorui2.confirm_value( + title=title, + value=intro_question, + description=None, + subtitle=None, + verb=verb, + info_button=True, + text_mono=False, + ) + ) + info_layout = RustLayout( + trezorui2.show_info_with_cancel( + title=address_title, + items=(("", address),), + chunkify=chunkify, + ) + ) + await raise_if_not_confirmed(with_info(msg_layout, info_layout, br_type, br_code)) + + # confirmation + if verb == TR.ethereum__staking_claim: + items = ((TR.send__maximum_fee, maximum_fee),) + else: + items = ( + (TR.words__amount + ":", total_amount), + (TR.send__maximum_fee, maximum_fee), + ) + await confirm_summary( + items, # items + title=title, + info_title=TR.confirm_total__title_fee, + info_items=info_items, + br_type=br_type, + br_code=br_code, + ) + + async def confirm_solana_tx( amount: str, fee: str, diff --git a/core/tests/test_apps.ethereum.layout.py b/core/tests/test_apps.ethereum.layout.py index f1b3477bd..d912a87c9 100644 --- a/core/tests/test_apps.ethereum.layout.py +++ b/core/tests/test_apps.ethereum.layout.py @@ -4,12 +4,11 @@ if not utils.BITCOIN_ONLY: from ethereum_common import make_network, make_token from apps.ethereum import networks - from apps.ethereum.layout import format_ethereum_amount + from apps.ethereum.helpers import format_ethereum_amount from apps.ethereum.tokens import UNKNOWN_TOKEN ETH = networks.by_chain_id(1) - @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") class TestFormatEthereumAmount(unittest.TestCase): def test_denominations(self): diff --git a/core/translations/en.json b/core/translations/en.json index 27c152e42..a38b08509 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -299,6 +299,14 @@ "ethereum__units_template": "{} units", "ethereum__unknown_token": "Unknown token", "ethereum__valid_signature": "The signature is valid.", + "ethereum__staking_stake": "STAKE", + "ethereum__staking_stake_address": "STAKE ADDRESS", + "ethereum__staking_stake_intro": "Stake ETH on Everstake?", + "ethereum__staking_unstake": "UNSTAKE", + "ethereum__staking_unstake_intro": "Unstake ETH from Everstake?", + "ethereum__staking_claim": "CLAIM", + "ethereum__staking_claim_address": "CLAIM ADDRESS", + "ethereum__staking_claim_intro": "Claim ETH from Everstake?", "experimental_mode__enable": "Enable experimental features?", "experimental_mode__only_for_dev": "Only for development and beta testing!", "experimental_mode__title": "EXPERIMENTAL MODE", diff --git a/core/translations/order.json b/core/translations/order.json index 198f6d9d9..f3c278375 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -832,5 +832,13 @@ "830": "words__writable", "831": "words__yes", "832": "reboot_to_bootloader__just_a_moment", - "833": "inputs__previous" + "833": "inputs__previous", + "834": "ethereum__staking_claim", + "835": "ethereum__staking_claim_address", + "836": "ethereum__staking_claim_intro", + "837": "ethereum__staking_stake", + "838": "ethereum__staking_stake_address", + "839": "ethereum__staking_stake_intro", + "840": "ethereum__staking_unstake", + "841": "ethereum__staking_unstake_intro" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 044ed3a82..97af8a5e2 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,9 +1,9 @@ { "current": { - "merkle_root": "4add9b7a2b80544a382378bc1abdae38600460825ef8010d45da5c2f28d86d26", + "merkle_root": "ebba747f556487944a26b19deb5910648694670f0de05f5a1569e1e12cf47ea0", "signature": null, - "datetime": "2024-02-21T09:03:23.136322", - "commit": "1dc00561ae04804aecbb0715d092c2a907e8eed8" + "datetime": "2024-02-21T15:09:10.231125", + "commit": "4204ba14044132269dc708e3f0b0cfac3bbfd906" }, "history": [] } diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py index 465bfb82b..5e578e4d8 100644 --- a/tests/device_tests/ethereum/test_signtx.py +++ b/tests/device_tests/ethereum/test_signtx.py @@ -454,3 +454,74 @@ def test_signtx_data_pagination(client: Client, flow): client.watch_layout() client.set_input_flow(flow(client, cancel=True)) _sign_tx_call() + + +@pytest.mark.skip_t1("T1 does not support Everstake") +@parametrize_using_common_fixtures("ethereum/sign_tx_staking.json") +# TODO input flows to go into info screens - then also parametrizing chunkify might make sense +# @pytest.mark.parametrize("chunkify", (True, False)) +def test_signtx_staking(client: Client, parameters: dict, result: dict): + with client: + sig_v, sig_r, sig_s = ethereum.sign_tx( + client, + n=parse_path(parameters["path"]), + nonce=int(parameters["nonce"], 16), + gas_price=int(parameters["gas_price"], 16), + gas_limit=int(parameters["gas_limit"], 16), + to=parameters["to_address"], + value=int(parameters["value"], 16), + data=bytes.fromhex(parameters["data"]), + chain_id=parameters["chain_id"], + tx_type=parameters["tx_type"], + definitions=None, + chunkify=False, + ) + expected_v = 2 * parameters["chain_id"] + 35 + assert sig_v in (expected_v, expected_v + 1) + assert sig_r.hex() == result["sig_r"] + assert sig_s.hex() == result["sig_s"] + assert sig_v == result["sig_v"] + + +@pytest.mark.skip_t1("T1 does not support Everstake") +@parametrize_using_common_fixtures("ethereum/sign_tx_staking_data_error.json") +def test_signtx_staking_bad_inputs(client: Client, parameters: dict, result: dict): + # result not needed + with pytest.raises(TrezorFailure, match=r"DataError"): + ethereum.sign_tx( + client, + n=parse_path(parameters["path"]), + nonce=int(parameters["nonce"], 16), + gas_price=int(parameters["gas_price"], 16), + gas_limit=int(parameters["gas_limit"], 16), + to=parameters["to_address"], + value=int(parameters["value"], 16), + data=bytes.fromhex(parameters["data"]), + chain_id=parameters["chain_id"], + tx_type=parameters["tx_type"], + definitions=None, + chunkify=False, + ) + + +@pytest.mark.skip_t1("T1 does not support Everstake") +@parametrize_using_common_fixtures("ethereum/sign_tx_staking_eip1559.json") +def test_signtx_staking_eip1559(client: Client, parameters: dict, result: dict): + with client: + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path(parameters["path"]), + nonce=int(parameters["nonce"], 16), + max_gas_fee=int(parameters["max_gas_fee"], 16), + max_priority_fee=int(parameters["max_priority_fee"], 16), + gas_limit=int(parameters["gas_limit"], 16), + to=parameters["to_address"], + value=int(parameters["value"], 16), + data=bytes.fromhex(parameters["data"]), + chain_id=parameters["chain_id"], + definitions=None, + chunkify=True, + ) + assert sig_r.hex() == result["sig_r"] + assert sig_s.hex() == result["sig_s"] + assert sig_v == result["sig_v"] diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index fd75b1883..c63f04b72 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -4507,6 +4507,23 @@ "TR_en_ethereum-test_signtx.py::test_signtx_eip1559_access_list_larger": "5ec441ee292a9034c7d859f216050e7af702dcc219ed16e4ca17352ae4784c9b", "TR_en_ethereum-test_signtx.py::test_signtx_fee_info": "b4ae728ff71c1e6112abbb0111b85b2760f957b677726b35734e63c318495408", "TR_en_ethereum-test_signtx.py::test_signtx_go_back_from_summary": "263993daffe2a77a46a17d5b598aca84de52ba0e051e4cb5de5c524a48192ed3", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[claim_holesky]": "8381fbc4aac0431757244c7813c33abee1a3457661a71b7b8b71fb4cd319d6f8", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[claim_mainnet]": "8381fbc4aac0431757244c7813c33abee1a3457661a71b7b8b71fb4cd319d6f8", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[stake_holesky]": "856bc0275109dcf0fbac25b20916ac78f358533e41e896b299959fbc0364a240", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[stake_main]": "856bc0275109dcf0fbac25b20916ac78f358533e41e896b299959fbc0364a240", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[unstake_holesky]": "4d80581b69013423285c924d837135a2c2b09b62cf2635fdbe3a8f224511bc7e", +"TR_en_ethereum-test_signtx.py::test_signtx_staking[unstake_main]": "4d80581b69013423285c924d837135a2c2b09b62cf2635fdbe3a8f224511bc7e", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[claim_bad_inputs_1]": "b70d9d2aa7a8ace3251763c1d2fcb53dd8c741b7520d717398df8f7ff8ac9128", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[stake_bad_inputs_1]": "b70d9d2aa7a8ace3251763c1d2fcb53dd8c741b7520d717398df8f7ff8ac9128", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[stake_bad_inputs_2]": "b70d9d2aa7a8ace3251763c1d2fcb53dd8c741b7520d717398df8f7ff8ac9128", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[unstake_bad_inputs_1]": "b70d9d2aa7a8ace3251763c1d2fcb53dd8c741b7520d717398df8f7ff8ac9128", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[unstake_bad_inputs_2]": "b70d9d2aa7a8ace3251763c1d2fcb53dd8c741b7520d717398df8f7ff8ac9128", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[claim_holesky]": "bf6067c40a106b593ecd43632308fd384a505be4b10269eae07090d1bb24b3f1", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[claim_mainnet]": "bf6067c40a106b593ecd43632308fd384a505be4b10269eae07090d1bb24b3f1", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[stake_holesky]": "5f0472c1ae8e221509b5568a89f7942e18caaa8e7b022d75519cfa0e671c94bc", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[stake_main]": "5f0472c1ae8e221509b5568a89f7942e18caaa8e7b022d75519cfa0e671c94bc", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[unstake_holesky]": "9c0d8add3295ed7de71bc13084e1216149af5cce259a72dc9c558c4fbfc9e479", +"TR_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[unstake_main]": "9c0d8add3295ed7de71bc13084e1216149af5cce259a72dc9c558c4fbfc9e479", "TR_en_misc-test_cosi.py::test_cosi_different_key": "8c801bd0142e5c1ad4aad50b34c7debb1b8f17a2e0a87eb7f95531b9fd15e095", "TR_en_misc-test_cosi.py::test_cosi_nonce": "df3420ca2395ced6fb2e3e5b984ece9d1a1151d877061681582c8f9404416600", "TR_en_misc-test_cosi.py::test_cosi_pubkey": "8c801bd0142e5c1ad4aad50b34c7debb1b8f17a2e0a87eb7f95531b9fd15e095", @@ -11564,6 +11581,23 @@ "TT_en_ethereum-test_signtx.py::test_signtx_eip1559_access_list_larger": "243010310ac5a4c70c627507ea8501cc61c2e20728eb06bc796f093132bebb4f", "TT_en_ethereum-test_signtx.py::test_signtx_fee_info": "714e4c5f6e6b45fa3e78f74c7ee5e3332f39686f8b708a4f56232105bde0c3e4", "TT_en_ethereum-test_signtx.py::test_signtx_go_back_from_summary": "8bc38a773c40a70c1eb9b91a5d02ce0a61591ce9e42bd0073bc1395f560f2490", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[claim_holesky]": "63a2b20a46d7eb9dbe188f45286f0e19b696b4fa072743156a1f70b8c33d5dad", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[claim_mainnet]": "63a2b20a46d7eb9dbe188f45286f0e19b696b4fa072743156a1f70b8c33d5dad", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[stake_holesky]": "f24ba4c504e12ec403aa99f19f9d9c78cc513edb2b7671063033902d089d894c", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[stake_main]": "f24ba4c504e12ec403aa99f19f9d9c78cc513edb2b7671063033902d089d894c", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[unstake_holesky]": "b24d5247a866e3aa69fe3fc17eabaa210890b285e3c4b84eb253570fcc0c8bed", +"TT_en_ethereum-test_signtx.py::test_signtx_staking[unstake_main]": "b24d5247a866e3aa69fe3fc17eabaa210890b285e3c4b84eb253570fcc0c8bed", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[claim_bad_inputs_1]": "3b6c5cf5c6512f1491b77f895d21d2f850f774c2b9d67c1b76eaeb2892e95e6b", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[stake_bad_inputs_1]": "3b6c5cf5c6512f1491b77f895d21d2f850f774c2b9d67c1b76eaeb2892e95e6b", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[stake_bad_inputs_2]": "3b6c5cf5c6512f1491b77f895d21d2f850f774c2b9d67c1b76eaeb2892e95e6b", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[unstake_bad_inputs_1]": "3b6c5cf5c6512f1491b77f895d21d2f850f774c2b9d67c1b76eaeb2892e95e6b", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_bad_inputs[unstake_bad_inputs_2]": "3b6c5cf5c6512f1491b77f895d21d2f850f774c2b9d67c1b76eaeb2892e95e6b", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[claim_holesky]": "63a2b20a46d7eb9dbe188f45286f0e19b696b4fa072743156a1f70b8c33d5dad", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[claim_mainnet]": "63a2b20a46d7eb9dbe188f45286f0e19b696b4fa072743156a1f70b8c33d5dad", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[stake_holesky]": "f24ba4c504e12ec403aa99f19f9d9c78cc513edb2b7671063033902d089d894c", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[stake_main]": "f24ba4c504e12ec403aa99f19f9d9c78cc513edb2b7671063033902d089d894c", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[unstake_holesky]": "b24d5247a866e3aa69fe3fc17eabaa210890b285e3c4b84eb253570fcc0c8bed", +"TT_en_ethereum-test_signtx.py::test_signtx_staking_eip1559[unstake_main]": "b24d5247a866e3aa69fe3fc17eabaa210890b285e3c4b84eb253570fcc0c8bed", "TT_en_misc-test_cosi.py::test_cosi_different_key": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_en_misc-test_cosi.py::test_cosi_nonce": "25a47ec1384fb563a6495d92d9319d19220cbb15b0f33fbdc26f01d3ccde1980", "TT_en_misc-test_cosi.py::test_cosi_pubkey": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", diff --git a/tools/check-bitcoin-only b/tools/check-bitcoin-only index f4e15af2a..116d6c045 100755 --- a/tools/check-bitcoin-only +++ b/tools/check-bitcoin-only @@ -6,7 +6,7 @@ EXCEPTIONS+=( "decred" ) # "decred" figures in field names used by the bitcoin EXCEPTIONS+=( "omni" ) # OMNI is part of the bitcoin app # BIP39 or SLIP39 words that have "dash" in them EXCEPTIONS+=( "dash" ) -EXCEPTIONS+=( "confirm_ethereum_tx" ) # is model-specific, so is in layout/__init__.py instead of ethereum/layout.py +EXCEPTIONS+=( "confirm_ethereum_tx" "confirm_ethereum_staking_tx" ) # model-specific, so is in layout/__init__.py instead of ethereum/layout.py EXCEPTIONS+=( "__" ) # ignoring the translations blob (section__key delimiter) EXCEPTIONS+=( "{}" ) # ignoring the translations blob (template identifier) diff --git a/vendor/fido2-tests b/vendor/fido2-tests index 9cfd22ef2..28e177c44 160000 --- a/vendor/fido2-tests +++ b/vendor/fido2-tests @@ -1 +1 @@ -Subproject commit 9cfd22ef20fec2c34d0f0e5c16a5d5152da30861 +Subproject commit 28e177c4424820aee8a6f031474c890e5bafe72c