From 9e478e9b46f5ab078bcab434fe132ec5016ec60d Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 15 Apr 2025 14:25:29 +0200 Subject: [PATCH] feat: Add "text details" memo to payment requests. --- common/protob/messages-common.proto | 6 + core/src/apps/common/payment_request.py | 8 + core/src/trezor/messages.py | 18 ++ python/src/trezorlib/messages.py | 20 ++ .../src/protos/generated/messages_common.rs | 269 +++++++++++++++++- tests/device_tests/payment_req.py | 13 + 6 files changed, 320 insertions(+), 14 deletions(-) diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto index 3b9743956b..1cee442f5a 100644 --- a/common/protob/messages-common.proto +++ b/common/protob/messages-common.proto @@ -188,12 +188,18 @@ message PaymentRequest { optional TextMemo text_memo = 1; optional RefundMemo refund_memo = 2; optional CoinPurchaseMemo coin_purchase_memo = 3; + optional TextDetailsMemo text_details_memo = 4; } message TextMemo { required string text = 1; // plain-text note explaining the purpose of the payment request } + message TextDetailsMemo { + optional string title = 1 [default=""]; // plain-text heading + optional string text = 2 [default=""]; // plain-text note containing additional details about the payment + } + message RefundMemo { required string address = 1; // the address where the payment should be refunded if necessary repeated uint32 address_n = 2; // BIP-32 path to derive the key from the master node diff --git a/core/src/apps/common/payment_request.py b/core/src/apps/common/payment_request.py index 8336b00c76..27b2deabfa 100644 --- a/core/src/apps/common/payment_request.py +++ b/core/src/apps/common/payment_request.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: _MEMO_TYPE_TEXT = const(1) _MEMO_TYPE_REFUND = const(2) _MEMO_TYPE_COIN_PURCHASE = const(3) +_MEMO_TYPE_TEXT_DETAILS = const(4) class PaymentRequestVerifier: @@ -73,6 +74,13 @@ class PaymentRequestVerifier: writers.write_uint32_le(self.h_pr, memo.coin_type) writers.write_bytes_prefixed(self.h_pr, memo.amount.encode()) writers.write_bytes_prefixed(self.h_pr, memo.address.encode()) + elif m.text_details_memo is not None: + memo = m.text_details_memo + writers.write_uint32_le(self.h_pr, _MEMO_TYPE_TEXT_DETAILS) + writers.write_bytes_prefixed(self.h_pr, memo.title.encode()) + writers.write_bytes_prefixed(self.h_pr, memo.text.encode()) + else: + DataError("Unrecognized memo type in payment request.") writers.write_uint32_le(self.h_pr, slip44_id) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index a417194d3a..6fd3f14c90 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -482,6 +482,7 @@ if TYPE_CHECKING: text_memo: "TextMemo | None" refund_memo: "RefundMemo | None" coin_purchase_memo: "CoinPurchaseMemo | None" + text_details_memo: "TextDetailsMemo | None" def __init__( self, @@ -489,6 +490,7 @@ if TYPE_CHECKING: text_memo: "TextMemo | None" = None, refund_memo: "RefundMemo | None" = None, coin_purchase_memo: "CoinPurchaseMemo | None" = None, + text_details_memo: "TextDetailsMemo | None" = None, ) -> None: pass @@ -510,6 +512,22 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["TextMemo"]: return isinstance(msg, cls) + class TextDetailsMemo(protobuf.MessageType): + title: "str" + text: "str" + + def __init__( + self, + *, + title: "str | None" = None, + text: "str | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TextDetailsMemo"]: + return isinstance(msg, cls) + class RefundMemo(protobuf.MessageType): address: "str" address_n: "list[int]" diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index ffe43ae29b..e5f3063bd3 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -1130,6 +1130,7 @@ class PaymentRequestMemo(protobuf.MessageType): 1: protobuf.Field("text_memo", "TextMemo", repeated=False, required=False, default=None), 2: protobuf.Field("refund_memo", "RefundMemo", repeated=False, required=False, default=None), 3: protobuf.Field("coin_purchase_memo", "CoinPurchaseMemo", repeated=False, required=False, default=None), + 4: protobuf.Field("text_details_memo", "TextDetailsMemo", repeated=False, required=False, default=None), } def __init__( @@ -1138,10 +1139,12 @@ class PaymentRequestMemo(protobuf.MessageType): text_memo: Optional["TextMemo"] = None, refund_memo: Optional["RefundMemo"] = None, coin_purchase_memo: Optional["CoinPurchaseMemo"] = None, + text_details_memo: Optional["TextDetailsMemo"] = None, ) -> None: self.text_memo = text_memo self.refund_memo = refund_memo self.coin_purchase_memo = coin_purchase_memo + self.text_details_memo = text_details_memo class TextMemo(protobuf.MessageType): @@ -1158,6 +1161,23 @@ class TextMemo(protobuf.MessageType): self.text = text +class TextDetailsMemo(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("title", "string", repeated=False, required=False, default=''), + 2: protobuf.Field("text", "string", repeated=False, required=False, default=''), + } + + def __init__( + self, + *, + title: Optional["str"] = '', + text: Optional["str"] = '', + ) -> None: + self.title = title + self.text = text + + class RefundMemo(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { diff --git a/rust/trezor-client/src/protos/generated/messages_common.rs b/rust/trezor-client/src/protos/generated/messages_common.rs index 076c2e59e9..90068259e7 100644 --- a/rust/trezor-client/src/protos/generated/messages_common.rs +++ b/rust/trezor-client/src/protos/generated/messages_common.rs @@ -2823,6 +2823,8 @@ pub mod payment_request { pub refund_memo: ::protobuf::MessageField, // @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.PaymentRequestMemo.coin_purchase_memo) pub coin_purchase_memo: ::protobuf::MessageField, + // @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.PaymentRequestMemo.text_details_memo) + pub text_details_memo: ::protobuf::MessageField, // special fields // @@protoc_insertion_point(special_field:hw.trezor.messages.common.PaymentRequest.PaymentRequestMemo.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -2840,7 +2842,7 @@ pub mod payment_request { } pub(in super) fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(3); + let mut fields = ::std::vec::Vec::with_capacity(4); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TextMemo>( "text_memo", @@ -2857,6 +2859,11 @@ pub mod payment_request { |m: &PaymentRequestMemo| { &m.coin_purchase_memo }, |m: &mut PaymentRequestMemo| { &mut m.coin_purchase_memo }, )); + fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, TextDetailsMemo>( + "text_details_memo", + |m: &PaymentRequestMemo| { &m.text_details_memo }, + |m: &mut PaymentRequestMemo| { &mut m.text_details_memo }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "PaymentRequest.PaymentRequestMemo", fields, @@ -2884,6 +2891,11 @@ pub mod payment_request { return false; } }; + for v in &self.text_details_memo { + if !v.is_initialized() { + return false; + } + }; true } @@ -2899,6 +2911,9 @@ pub mod payment_request { 26 => { ::protobuf::rt::read_singular_message_into_field(is, &mut self.coin_purchase_memo)?; }, + 34 => { + ::protobuf::rt::read_singular_message_into_field(is, &mut self.text_details_memo)?; + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -2923,6 +2938,10 @@ pub mod payment_request { let len = v.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; } + if let Some(v) = self.text_details_memo.as_ref() { + let len = v.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -2938,6 +2957,9 @@ pub mod payment_request { if let Some(v) = self.coin_purchase_memo.as_ref() { ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; } + if let Some(v) = self.text_details_memo.as_ref() { + ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -2958,6 +2980,7 @@ pub mod payment_request { self.text_memo.clear(); self.refund_memo.clear(); self.coin_purchase_memo.clear(); + self.text_details_memo.clear(); self.special_fields.clear(); } @@ -2966,6 +2989,7 @@ pub mod payment_request { text_memo: ::protobuf::MessageField::none(), refund_memo: ::protobuf::MessageField::none(), coin_purchase_memo: ::protobuf::MessageField::none(), + text_details_memo: ::protobuf::MessageField::none(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -3150,6 +3174,218 @@ pub mod payment_request { type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; } + // @@protoc_insertion_point(message:hw.trezor.messages.common.PaymentRequest.TextDetailsMemo) + #[derive(PartialEq,Clone,Default,Debug)] + pub struct TextDetailsMemo { + // message fields + // @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.TextDetailsMemo.title) + pub title: ::std::option::Option<::std::string::String>, + // @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.TextDetailsMemo.text) + pub text: ::std::option::Option<::std::string::String>, + // special fields + // @@protoc_insertion_point(special_field:hw.trezor.messages.common.PaymentRequest.TextDetailsMemo.special_fields) + pub special_fields: ::protobuf::SpecialFields, + } + + impl<'a> ::std::default::Default for &'a TextDetailsMemo { + fn default() -> &'a TextDetailsMemo { + ::default_instance() + } + } + + impl TextDetailsMemo { + pub fn new() -> TextDetailsMemo { + ::std::default::Default::default() + } + + // optional string title = 1; + + pub fn title(&self) -> &str { + match self.title.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_title(&mut self) { + self.title = ::std::option::Option::None; + } + + pub fn has_title(&self) -> bool { + self.title.is_some() + } + + // Param is passed by value, moved + pub fn set_title(&mut self, v: ::std::string::String) { + self.title = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_title(&mut self) -> &mut ::std::string::String { + if self.title.is_none() { + self.title = ::std::option::Option::Some(::std::string::String::new()); + } + self.title.as_mut().unwrap() + } + + // Take field + pub fn take_title(&mut self) -> ::std::string::String { + self.title.take().unwrap_or_else(|| ::std::string::String::new()) + } + + // optional string text = 2; + + pub fn text(&self) -> &str { + match self.text.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_text(&mut self) { + self.text = ::std::option::Option::None; + } + + pub fn has_text(&self) -> bool { + self.text.is_some() + } + + // Param is passed by value, moved + pub fn set_text(&mut self, v: ::std::string::String) { + self.text = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_text(&mut self) -> &mut ::std::string::String { + if self.text.is_none() { + self.text = ::std::option::Option::Some(::std::string::String::new()); + } + self.text.as_mut().unwrap() + } + + // Take field + pub fn take_text(&mut self) -> ::std::string::String { + self.text.take().unwrap_or_else(|| ::std::string::String::new()) + } + + pub(in super) fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { + let mut fields = ::std::vec::Vec::with_capacity(2); + let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "title", + |m: &TextDetailsMemo| { &m.title }, + |m: &mut TextDetailsMemo| { &mut m.title }, + )); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "text", + |m: &TextDetailsMemo| { &m.text }, + |m: &mut TextDetailsMemo| { &mut m.text }, + )); + ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( + "PaymentRequest.TextDetailsMemo", + fields, + oneofs, + ) + } + } + + impl ::protobuf::Message for TextDetailsMemo { + const NAME: &'static str = "TextDetailsMemo"; + + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.title = ::std::option::Option::Some(is.read_string()?); + }, + 18 => { + self.text = ::std::option::Option::Some(is.read_string()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.title.as_ref() { + my_size += ::protobuf::rt::string_size(1, &v); + } + if let Some(v) = self.text.as_ref() { + my_size += ::protobuf::rt::string_size(2, &v); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.title.as_ref() { + os.write_string(1, v)?; + } + if let Some(v) = self.text.as_ref() { + os.write_string(2, v)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> TextDetailsMemo { + TextDetailsMemo::new() + } + + fn clear(&mut self) { + self.title = ::std::option::Option::None; + self.text = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static TextDetailsMemo { + static instance: TextDetailsMemo = TextDetailsMemo { + title: ::std::option::Option::None, + text: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } + } + + impl ::protobuf::MessageFull for TextDetailsMemo { + fn descriptor() -> ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| super::file_descriptor().message_by_package_relative_name("PaymentRequest.TextDetailsMemo").unwrap()).clone() + } + } + + impl ::std::fmt::Display for TextDetailsMemo { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } + } + + impl ::protobuf::reflect::ProtobufValue for TextDetailsMemo { + type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; + } + // @@protoc_insertion_point(message:hw.trezor.messages.common.PaymentRequest.RefundMemo) #[derive(PartialEq,Clone,Default,Debug)] pub struct RefundMemo { @@ -3776,26 +4012,30 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x02(\rR\x0bfingerprint\x12\x1b\n\tchild_num\x18\x03\x20\x02(\rR\x08chil\ dNum\x12\x1d\n\nchain_code\x18\x04\x20\x02(\x0cR\tchainCode\x12\x1f\n\ \x0bprivate_key\x18\x05\x20\x01(\x0cR\nprivateKey\x12\x1d\n\npublic_key\ - \x18\x06\x20\x02(\x0cR\tpublicKey\"\x90\x06\n\x0ePaymentRequest\x12\x14\ + \x18\x06\x20\x02(\x0cR\tpublicKey\"\xb8\x07\n\x0ePaymentRequest\x12\x14\ \n\x05nonce\x18\x01\x20\x01(\x0cR\x05nonce\x12%\n\x0erecipient_name\x18\ \x02\x20\x02(\tR\rrecipientName\x12R\n\x05memos\x18\x03\x20\x03(\x0b2<.h\ w.trezor.messages.common.PaymentRequest.PaymentRequestMemoR\x05memos\x12\ \x16\n\x06amount\x18\x04\x20\x01(\x04R\x06amount\x12\x1c\n\tsignature\ - \x18\x05\x20\x02(\x0cR\tsignature\x1a\xa6\x02\n\x12PaymentRequestMemo\ + \x18\x05\x20\x02(\x0cR\tsignature\x1a\x8d\x03\n\x12PaymentRequestMemo\ \x12O\n\ttext_memo\x18\x01\x20\x01(\x0b22.hw.trezor.messages.common.Paym\ entRequest.TextMemoR\x08textMemo\x12U\n\x0brefund_memo\x18\x02\x20\x01(\ \x0b24.hw.trezor.messages.common.PaymentRequest.RefundMemoR\nrefundMemo\ \x12h\n\x12coin_purchase_memo\x18\x03\x20\x01(\x0b2:.hw.trezor.messages.\ - common.PaymentRequest.CoinPurchaseMemoR\x10coinPurchaseMemo\x1a\x1e\n\ - \x08TextMemo\x12\x12\n\x04text\x18\x01\x20\x02(\tR\x04text\x1aU\n\nRefun\ - dMemo\x12\x18\n\x07address\x18\x01\x20\x02(\tR\x07address\x12\x1b\n\tadd\ - ress_n\x18\x02\x20\x03(\rR\x08addressN\x12\x10\n\x03mac\x18\x03\x20\x02(\ - \x0cR\x03mac\x1a\x90\x01\n\x10CoinPurchaseMemo\x12\x1b\n\tcoin_type\x18\ - \x01\x20\x02(\rR\x08coinType\x12\x16\n\x06amount\x18\x02\x20\x02(\tR\x06\ - amount\x12\x18\n\x07address\x18\x03\x20\x02(\tR\x07address\x12\x1b\n\tad\ - dress_n\x18\x04\x20\x03(\rR\x08addressN\x12\x10\n\x03mac\x18\x05\x20\x02\ - (\x0cR\x03mac:\x04\x88\xb2\x19\x01B>\n#com.satoshilabs.trezor.lib.protob\ - ufB\x13TrezorMessageCommon\x80\xa6\x1d\x01\ + common.PaymentRequest.CoinPurchaseMemoR\x10coinPurchaseMemo\x12e\n\x11te\ + xt_details_memo\x18\x04\x20\x01(\x0b29.hw.trezor.messages.common.Payment\ + Request.TextDetailsMemoR\x0ftextDetailsMemo\x1a\x1e\n\x08TextMemo\x12\ + \x12\n\x04text\x18\x01\x20\x02(\tR\x04text\x1a?\n\x0fTextDetailsMemo\x12\ + \x16\n\x05title\x18\x01\x20\x01(\t:\0R\x05title\x12\x14\n\x04text\x18\ + \x02\x20\x01(\t:\0R\x04text\x1aU\n\nRefundMemo\x12\x18\n\x07address\x18\ + \x01\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x02\x20\x03(\rR\x08\ + addressN\x12\x10\n\x03mac\x18\x03\x20\x02(\x0cR\x03mac\x1a\x90\x01\n\x10\ + CoinPurchaseMemo\x12\x1b\n\tcoin_type\x18\x01\x20\x02(\rR\x08coinType\ + \x12\x16\n\x06amount\x18\x02\x20\x02(\tR\x06amount\x12\x18\n\x07address\ + \x18\x03\x20\x02(\tR\x07address\x12\x1b\n\taddress_n\x18\x04\x20\x03(\rR\ + \x08addressN\x12\x10\n\x03mac\x18\x05\x20\x02(\x0cR\x03mac:\x04\x88\xb2\ + \x19\x01B>\n#com.satoshilabs.trezor.lib.protobufB\x13TrezorMessageCommon\ + \x80\xa6\x1d\x01\ "; /// `FileDescriptorProto` object which was a source for this generated file @@ -3814,7 +4054,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { let mut deps = ::std::vec::Vec::with_capacity(1); deps.push(super::options::file_descriptor().clone()); - let mut messages = ::std::vec::Vec::with_capacity(16); + let mut messages = ::std::vec::Vec::with_capacity(17); messages.push(Success::generated_message_descriptor_data()); messages.push(Failure::generated_message_descriptor_data()); messages.push(ButtonRequest::generated_message_descriptor_data()); @@ -3829,6 +4069,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { messages.push(PaymentRequest::generated_message_descriptor_data()); messages.push(payment_request::PaymentRequestMemo::generated_message_descriptor_data()); messages.push(payment_request::TextMemo::generated_message_descriptor_data()); + messages.push(payment_request::TextDetailsMemo::generated_message_descriptor_data()); messages.push(payment_request::RefundMemo::generated_message_descriptor_data()); messages.push(payment_request::CoinPurchaseMemo::generated_message_descriptor_data()); let mut enums = ::std::vec::Vec::with_capacity(3); diff --git a/tests/device_tests/payment_req.py b/tests/device_tests/payment_req.py index f83114f0c3..b7341a1788 100644 --- a/tests/device_tests/payment_req.py +++ b/tests/device_tests/payment_req.py @@ -13,6 +13,12 @@ class TextMemo: text: str +@dataclass +class TextDetailsMemo: + title: str + text: str + + @dataclass class RefundMemo: address_n: list[int] @@ -94,6 +100,13 @@ def make_payment_request( h_pr.update(memo.slip44.to_bytes(4, "little")) hash_bytes_prefixed(h_pr, memo.amount.encode()) hash_bytes_prefixed(h_pr, memo.address_resp.address.encode()) + elif isinstance(memo, TextDetailsMemo): + msg_memo = messages.TextDetailsMemo(text=memo.text) + msg_memos.append(messages.PaymentRequestMemo(text_memo=msg_memo)) + memo_type = 4 + h_pr.update(memo_type.to_bytes(4, "little")) + hash_bytes_prefixed(h_pr, memo.title.encode()) + hash_bytes_prefixed(h_pr, memo.text.encode()) else: raise ValueError