feat(core): confirm ETH stake, unstake, claim

pull/3546/merge
obrusvit 4 months ago committed by Vít Obrusník
parent e1f696b4dd
commit ebcf3e2db2

@ -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"
}
}
]
}

@ -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"
}
}
]
}

@ -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"
}
}
]
}

@ -0,0 +1 @@
Clear sign ETH staking transactions on Everstake pool.

@ -367,6 +367,14 @@ static void _librust_qstrs(void) {
MP_QSTR_ethereum__show_full_message; MP_QSTR_ethereum__show_full_message;
MP_QSTR_ethereum__show_full_struct; MP_QSTR_ethereum__show_full_struct;
MP_QSTR_ethereum__sign_eip712; 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_data;
MP_QSTR_ethereum__title_confirm_domain; MP_QSTR_ethereum__title_confirm_domain;
MP_QSTR_ethereum__title_confirm_message; MP_QSTR_ethereum__title_confirm_message;
@ -898,6 +906,7 @@ static void _librust_qstrs(void) {
MP_QSTR_stellar__your_account; MP_QSTR_stellar__your_account;
MP_QSTR_subprompt; MP_QSTR_subprompt;
MP_QSTR_subtitle; MP_QSTR_subtitle;
MP_QSTR_text_mono;
MP_QSTR_tezos__baker_address; MP_QSTR_tezos__baker_address;
MP_QSTR_tezos__balance; MP_QSTR_tezos__balance;
MP_QSTR_tezos__ballot; MP_QSTR_tezos__ballot;

@ -844,6 +844,14 @@ pub enum TranslatedString {
words__yes = 831, words__yes = 831,
reboot_to_bootloader__just_a_moment = 832, reboot_to_bootloader__just_a_moment = 832,
inputs__previous = 833, 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 { impl TranslatedString {
@ -1683,6 +1691,14 @@ impl TranslatedString {
Self::words__yes => "Yes", Self::words__yes => "Yes",
Self::reboot_to_bootloader__just_a_moment => "Just a moment...", Self::reboot_to_bootloader__just_a_moment => "Just a moment...",
Self::inputs__previous => "PREVIOUS", 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_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_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_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, _ => None,
} }
} }

@ -463,6 +463,7 @@ struct ConfirmBlobParams {
info_button: bool, info_button: bool,
hold: bool, hold: bool,
chunkify: bool, chunkify: bool,
text_mono: bool,
} }
impl ConfirmBlobParams { impl ConfirmBlobParams {
@ -485,6 +486,7 @@ impl ConfirmBlobParams {
info_button: false, info_button: false,
hold, hold,
chunkify: false, chunkify: false,
text_mono: true,
} }
} }
@ -508,6 +510,11 @@ impl ConfirmBlobParams {
self self
} }
fn with_text_mono(mut self, text_mono: bool) -> Self {
self.text_mono = text_mono;
self
}
fn into_layout(self) -> Result<Obj, Error> { fn into_layout(self) -> Result<Obj, Error> {
let paragraphs = ConfirmBlob { let paragraphs = ConfirmBlob {
description: self.description.unwrap_or_else(StrBuffer::empty), description: self.description.unwrap_or_else(StrBuffer::empty),
@ -518,8 +525,10 @@ impl ConfirmBlobParams {
data_font: if self.chunkify { data_font: if self.chunkify {
let data: StrBuffer = self.data.try_into()?; let data: StrBuffer = self.data.try_into()?;
theme::get_chunkified_text_style(data.len()) theme::get_chunkified_text_style(data.len())
} else { } else if self.text_mono {
&theme::TEXT_MONO &theme::TEXT_MONO
} else {
&theme::TEXT_NORMAL
}, },
} }
.into_paragraphs(); .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 title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let horizontal: bool = kwargs.get_or(Qstr::MP_QSTR_horizontal, false)?; 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(); 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 key: StrBuffer = key.try_into()?;
let value: StrBuffer = value.try_into()?; let value: StrBuffer = value.try_into()?;
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, key).no_break()); 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 { 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()?; .try_into_option()?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, 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) ConfirmBlobParams::new(title, value, description, verb, verb_cancel, hold)
.with_subtitle(subtitle) .with_subtitle(subtitle)
.with_info_button(info_button) .with_info_button(info_button)
.with_chunkify(chunkify) .with_chunkify(chunkify)
.with_text_mono(text_mono)
.into_layout() .into_layout()
}; };
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } 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, /// title: str,
/// items: Iterable[Tuple[str, str]], /// items: Iterable[Tuple[str, str]],
/// horizontal: bool = False, /// horizontal: bool = False,
/// chunkify: bool = False,
/// ) -> object: /// ) -> object:
/// """Show metadata for outgoing transaction.""" /// """Show metadata for outgoing transaction."""
Qstr::MP_QSTR_show_info_with_cancel => obj_fn_kw!(0, new_show_info_with_cancel).as_obj(), 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, /// info_button: bool = False,
/// hold: bool = False, /// hold: bool = False,
/// chunkify: bool = False, /// chunkify: bool = False,
/// text_mono: bool = True,
/// ) -> object: /// ) -> object:
/// """Confirm value. Merge of confirm_total and confirm_output.""" /// """Confirm value. Merge of confirm_total and confirm_output."""
Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(), Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(),

@ -574,6 +574,7 @@ def show_info_with_cancel(
title: str, title: str,
items: Iterable[Tuple[str, str]], items: Iterable[Tuple[str, str]],
horizontal: bool = False, horizontal: bool = False,
chunkify: bool = False,
) -> object: ) -> object:
"""Show metadata for outgoing transaction.""" """Show metadata for outgoing transaction."""
@ -590,6 +591,7 @@ def confirm_value(
info_button: bool = False, info_button: bool = False,
hold: bool = False, hold: bool = False,
chunkify: bool = False, chunkify: bool = False,
text_mono: bool = True,
) -> object: ) -> object:
"""Confirm value. Merge of confirm_total and confirm_output.""" """Confirm value. Merge of confirm_total and confirm_output."""

@ -288,6 +288,14 @@ class TR:
ethereum__show_full_message: str = "Show full message" ethereum__show_full_message: str = "Show full message"
ethereum__show_full_struct: str = "Show full struct" ethereum__show_full_struct: str = "Show full struct"
ethereum__sign_eip712: str = "Really sign EIP-712 typed data?" 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_data: str = "CONFIRM DATA"
ethereum__title_confirm_domain: str = "CONFIRM DOMAIN" ethereum__title_confirm_domain: str = "CONFIRM DOMAIN"
ethereum__title_confirm_message: str = "CONFIRM MESSAGE" ethereum__title_confirm_message: str = "CONFIRM MESSAGE"

@ -535,6 +535,8 @@ if not utils.BITCOIN_ONLY:
import apps.ethereum.sign_tx_eip1559 import apps.ethereum.sign_tx_eip1559
apps.ethereum.sign_typed_data apps.ethereum.sign_typed_data
import 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 apps.ethereum.tokens
import apps.ethereum.tokens import apps.ethereum.tokens
apps.ethereum.verify_message apps.ethereum.verify_message

@ -1,13 +1,16 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ubinascii import hexlify from ubinascii import hexlify
from trezor import TR
from . import networks from . import networks
if TYPE_CHECKING: 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) 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 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: def _from_bytes_bigendian_signed(b: bytes) -> int:
negative = b[0] & 0x80 negative = b[0] & 0x80
if negative: if negative:

@ -4,12 +4,12 @@ from trezor import TR, ui
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezor.ui.layouts import ( from trezor.ui.layouts import (
confirm_blob, confirm_blob,
confirm_ethereum_tx, confirm_ethereum_staking_tx,
confirm_text, confirm_text,
should_show_more, 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: if TYPE_CHECKING:
from typing import Awaitable, Iterable from typing import Awaitable, Iterable
@ -25,12 +25,14 @@ if TYPE_CHECKING:
async def require_confirm_tx( async def require_confirm_tx(
to_bytes: bytes, to_bytes: bytes,
value: int, value: int,
gas_price: int, maximum_fee: str,
gas_limit: int, fee_info_items: Iterable[tuple[str, str]],
network: EthereumNetworkInfo, network: EthereumNetworkInfo,
token: EthereumTokenInfo | None, token: EthereumTokenInfo | None,
chunkify: bool, chunkify: bool,
) -> None: ) -> None:
from trezor.ui.layouts import confirm_ethereum_tx
if to_bytes: if to_bytes:
to_str = address_from_bytes(to_bytes, network) to_str = address_from_bytes(to_bytes, network)
else: else:
@ -38,56 +40,80 @@ async def require_confirm_tx(
chunkify = False chunkify = False
total_amount = format_ethereum_amount(value, token, network) 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( 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( async def require_confirm_stake(
to_bytes: bytes, addr_bytes: bytes,
value: int, value: int,
max_gas_fee: int, maximum_fee: str,
max_priority_fee: int, fee_info_items: Iterable[tuple[str, str]],
gas_limit: int,
network: EthereumNetworkInfo, network: EthereumNetworkInfo,
token: EthereumTokenInfo | None,
chunkify: bool, chunkify: bool,
) -> None: ) -> 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) addr_str = address_from_bytes(addr_bytes, network)
maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, network) total_amount = format_ethereum_amount(value, None, network)
gas_limit_str = TR.ethereum__units_template.format(gas_limit) await confirm_ethereum_staking_tx(
max_gas_fee_str = format_ethereum_amount( TR.ethereum__staking_stake, # title
max_gas_fee, None, network, force_unit_gwei=True TR.ethereum__staking_stake_intro, # intro_question
) TR.ethereum__staking_stake, # verb
max_priority_fee_str = format_ethereum_amount( total_amount, # total_amount
max_priority_fee, None, network, force_unit_gwei=True 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), async def require_confirm_unstake(
(TR.ethereum__max_gas_price, max_gas_fee_str), addr_bytes: bytes,
(TR.ethereum__priority_fee, max_priority_fee_str), 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( return confirm_blob(
"confirm_data", "confirm_data",
TR.ethereum__title_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: def limit_str(s: str, limit: int = 16) -> str:
"""Shortens string to show the last <limit> characters.""" """Shortens string to show the last <limit> characters."""
if len(s) <= limit + 2: if len(s) <= limit + 2:

@ -2,13 +2,23 @@ from typing import TYPE_CHECKING
from trezor.crypto import rlp from trezor.crypto import rlp
from trezor.messages import EthereumTxRequest from trezor.messages import EthereumTxRequest
from trezor.utils import BufferReader
from trezor.wire import DataError from trezor.wire import DataError
from apps.ethereum import staking_tx_constants as constants
from .helpers import bytes_from_address from .helpers import bytes_from_address
from .keychain import with_keychain_from_chain_id from .keychain import with_keychain_from_chain_id
if TYPE_CHECKING: 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 from apps.common.keychain import Keychain
@ -33,7 +43,9 @@ async def sign_tx(
from apps.common import paths 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 # check
if msg.tx_type not in [1, 6, None]: if msg.tx_type not in [1, 6, None]:
@ -42,26 +54,20 @@ async def sign_tx(
raise DataError("Fee overflow") raise DataError("Fee overflow")
check_common_fields(msg) check_common_fields(msg)
# have a user confirm signing
await paths.validate_path(keychain, msg.address_n) await paths.validate_path(keychain, msg.address_n)
address_bytes = bytes_from_address(msg.to)
# Handle ERC20s gas_price = int.from_bytes(msg.gas_price, "big")
token, address_bytes, recipient, value = await handle_erc20(msg, defs) gas_limit = int.from_bytes(msg.gas_limit, "big")
maximum_fee = format_ethereum_amount(gas_price * gas_limit, None, defs.network)
data_total = msg.data_length # local_cache_attribute fee_items = get_fee_items_regular(
gas_price,
if token is None and data_total > 0: gas_limit,
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"),
defs.network, defs.network,
token,
bool(msg.chunkify),
) )
await confirm_tx_data(msg, defs, address_bytes, maximum_fee, fee_items, data_total)
# sign
data = bytearray() data = bytearray()
data += msg.data_initial_chunk data += msg.data_initial_chunk
data_left = data_total - len(msg.data_initial_chunk) data_left = data_total - len(msg.data_initial_chunk)
@ -99,16 +105,89 @@ async def sign_tx(
return result 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, msg: MsgInSignTx,
definitions: Definitions, definitions: Definitions,
) -> tuple[EthereumTokenInfo | None, bytes, bytes, int]: address_bytes: bytes,
) -> tuple[EthereumTokenInfo | None, bytes, int]:
from . import tokens from . import tokens
from .layout import require_confirm_unknown_token from .layout import require_confirm_unknown_token
data_initial_chunk = msg.data_initial_chunk # local_cache_attribute data_initial_chunk = msg.data_initial_chunk # local_cache_attribute
token = None token = None
address_bytes = recipient = bytes_from_address(msg.to) recipient = address_bytes
value = int.from_bytes(msg.value, "big") value = int.from_bytes(msg.value, "big")
if ( if (
len(msg.to) in (40, 42) len(msg.to) in (40, 42)
@ -125,7 +204,7 @@ async def handle_erc20(
if token is tokens.UNKNOWN_TOKEN: if token is tokens.UNKNOWN_TOKEN:
await require_confirm_unknown_token(address_bytes) 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: 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: if msg.chain_id == 0:
raise DataError("Chain ID out of bounds") 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)

@ -42,8 +42,8 @@ async def sign_tx_eip1559(
from apps.common import paths from apps.common import paths
from .layout import require_confirm_data, require_confirm_tx_eip1559 from .helpers import format_ethereum_amount, get_fee_items_eip1559
from .sign_tx import check_common_fields, handle_erc20, send_request_chunk from .sign_tx import check_common_fields, confirm_tx_data, send_request_chunk
gas_limit = msg.gas_limit # local_cache_attribute gas_limit = msg.gas_limit # local_cache_attribute
data_total = msg.data_length # 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") raise wire.DataError("Fee overflow")
check_common_fields(msg) check_common_fields(msg)
# have a user confirm signing
await paths.validate_path(keychain, msg.address_n) await paths.validate_path(keychain, msg.address_n)
address_bytes = bytes_from_address(msg.to)
# Handle ERC20s
token, address_bytes, recipient, value = await handle_erc20(msg, defs) max_gas_fee = int.from_bytes(msg.max_gas_fee, "big")
max_priority_fee = int.from_bytes(msg.max_priority_fee, "big")
if token is None and data_total > 0: gas_limit = int.from_bytes(msg.gas_limit, "big")
await require_confirm_data(msg.data_initial_chunk, data_total) maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, defs.network)
fee_items = get_fee_items_eip1559(
await require_confirm_tx_eip1559( max_gas_fee,
recipient, max_priority_fee,
value, gas_limit,
int.from_bytes(msg.max_gas_fee, "big"),
int.from_bytes(msg.max_priority_fee, "big"),
int.from_bytes(gas_limit, "big"),
defs.network, 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 = bytearray()
data += msg.data_initial_chunk data += msg.data_initial_chunk
data_left = data_total - len(msg.data_initial_chunk) data_left = data_total - len(msg.data_initial_chunk)

@ -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
)

@ -1023,7 +1023,7 @@ async def confirm_value(
title=info_title.upper(), title=info_title.upper(),
action=info_value, action=info_value,
description=description, description=description,
verb=TR.buttons__back, verb="",
verb_cancel="<", verb_cancel="<",
hold=False, hold=False,
reverse=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( async def confirm_solana_tx(
amount: str, amount: str,
fee: str, fee: str,

@ -859,6 +859,7 @@ def confirm_value(
verb: str | None = None, verb: str | None = None,
subtitle: str | None = None, subtitle: str | None = None,
hold: bool = False, hold: bool = False,
value_text_mono: bool = True,
info_items: Iterable[tuple[str, str]] | None = None, info_items: Iterable[tuple[str, str]] | None = None,
) -> Awaitable[None]: ) -> Awaitable[None]:
"""General confirmation dialog, used by many other confirm_* functions.""" """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)) info_items.append((TR.confirm_total__fee_rate, fee_rate_amount))
await confirm_summary( 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]], items: Iterable[tuple[str, str]],
title: str | None = None, title: str | None = None,
info_items: Iterable[tuple[str, str]] | None = None, info_items: Iterable[tuple[str, str]] | None = None,
info_title: str | None = None,
br_type: str = "confirm_total", br_type: str = "confirm_total",
br_code: ButtonRequestType = ButtonRequestType.SignTx, br_code: ButtonRequestType = ButtonRequestType.SignTx,
) -> None: ) -> None:
@ -971,7 +977,7 @@ async def confirm_summary(
info_items = info_items or [] info_items = info_items or []
info_layout = RustLayout( info_layout = RustLayout(
trezorui2.show_info_with_cancel( 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, items=info_items,
) )
) )
@ -1025,6 +1031,60 @@ async def confirm_ethereum_tx(
continue 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( async def confirm_solana_tx(
amount: str, amount: str,
fee: str, fee: str,

@ -4,12 +4,11 @@ if not utils.BITCOIN_ONLY:
from ethereum_common import make_network, make_token from ethereum_common import make_network, make_token
from apps.ethereum import networks 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 from apps.ethereum.tokens import UNKNOWN_TOKEN
ETH = networks.by_chain_id(1) ETH = networks.by_chain_id(1)
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") @unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestFormatEthereumAmount(unittest.TestCase): class TestFormatEthereumAmount(unittest.TestCase):
def test_denominations(self): def test_denominations(self):

@ -299,6 +299,14 @@
"ethereum__units_template": "{} units", "ethereum__units_template": "{} units",
"ethereum__unknown_token": "Unknown token", "ethereum__unknown_token": "Unknown token",
"ethereum__valid_signature": "The signature is valid.", "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__enable": "Enable experimental features?",
"experimental_mode__only_for_dev": "Only for development and beta testing!", "experimental_mode__only_for_dev": "Only for development and beta testing!",
"experimental_mode__title": "EXPERIMENTAL MODE", "experimental_mode__title": "EXPERIMENTAL MODE",

@ -832,5 +832,13 @@
"830": "words__writable", "830": "words__writable",
"831": "words__yes", "831": "words__yes",
"832": "reboot_to_bootloader__just_a_moment", "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"
} }

@ -1,9 +1,9 @@
{ {
"current": { "current": {
"merkle_root": "4add9b7a2b80544a382378bc1abdae38600460825ef8010d45da5c2f28d86d26", "merkle_root": "ebba747f556487944a26b19deb5910648694670f0de05f5a1569e1e12cf47ea0",
"signature": null, "signature": null,
"datetime": "2024-02-21T09:03:23.136322", "datetime": "2024-02-21T15:09:10.231125",
"commit": "1dc00561ae04804aecbb0715d092c2a907e8eed8" "commit": "4204ba14044132269dc708e3f0b0cfac3bbfd906"
}, },
"history": [] "history": []
} }

@ -454,3 +454,74 @@ def test_signtx_data_pagination(client: Client, flow):
client.watch_layout() client.watch_layout()
client.set_input_flow(flow(client, cancel=True)) client.set_input_flow(flow(client, cancel=True))
_sign_tx_call() _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"]

@ -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_eip1559_access_list_larger": "5ec441ee292a9034c7d859f216050e7af702dcc219ed16e4ca17352ae4784c9b",
"TR_en_ethereum-test_signtx.py::test_signtx_fee_info": "b4ae728ff71c1e6112abbb0111b85b2760f957b677726b35734e63c318495408", "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_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_different_key": "8c801bd0142e5c1ad4aad50b34c7debb1b8f17a2e0a87eb7f95531b9fd15e095",
"TR_en_misc-test_cosi.py::test_cosi_nonce": "df3420ca2395ced6fb2e3e5b984ece9d1a1151d877061681582c8f9404416600", "TR_en_misc-test_cosi.py::test_cosi_nonce": "df3420ca2395ced6fb2e3e5b984ece9d1a1151d877061681582c8f9404416600",
"TR_en_misc-test_cosi.py::test_cosi_pubkey": "8c801bd0142e5c1ad4aad50b34c7debb1b8f17a2e0a87eb7f95531b9fd15e095", "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_eip1559_access_list_larger": "243010310ac5a4c70c627507ea8501cc61c2e20728eb06bc796f093132bebb4f",
"TT_en_ethereum-test_signtx.py::test_signtx_fee_info": "714e4c5f6e6b45fa3e78f74c7ee5e3332f39686f8b708a4f56232105bde0c3e4", "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_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_different_key": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3",
"TT_en_misc-test_cosi.py::test_cosi_nonce": "25a47ec1384fb563a6495d92d9319d19220cbb15b0f33fbdc26f01d3ccde1980", "TT_en_misc-test_cosi.py::test_cosi_nonce": "25a47ec1384fb563a6495d92d9319d19220cbb15b0f33fbdc26f01d3ccde1980",
"TT_en_misc-test_cosi.py::test_cosi_pubkey": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_en_misc-test_cosi.py::test_cosi_pubkey": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3",

@ -6,7 +6,7 @@ EXCEPTIONS+=( "decred" ) # "decred" figures in field names used by the bitcoin
EXCEPTIONS+=( "omni" ) # OMNI is part of the bitcoin app EXCEPTIONS+=( "omni" ) # OMNI is part of the bitcoin app
# BIP39 or SLIP39 words that have "dash" in them # BIP39 or SLIP39 words that have "dash" in them
EXCEPTIONS+=( "dash" ) 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 (section__key delimiter)
EXCEPTIONS+=( "{}" ) # ignoring the translations blob (template identifier) EXCEPTIONS+=( "{}" ) # ignoring the translations blob (template identifier)

@ -1 +1 @@
Subproject commit 9cfd22ef20fec2c34d0f0e5c16a5d5152da30861 Subproject commit 28e177c4424820aee8a6f031474c890e5bafe72c
Loading…
Cancel
Save