1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-21 09:39:02 +00:00

feat: Add "text details" memo to payment requests.

This commit is contained in:
Andrew Kozlik 2025-04-15 14:25:29 +02:00
parent d156a629fa
commit 9e478e9b46
6 changed files with 320 additions and 14 deletions

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

@ -2823,6 +2823,8 @@ pub mod payment_request {
pub refund_memo: ::protobuf::MessageField<RefundMemo>,
// @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.PaymentRequestMemo.coin_purchase_memo)
pub coin_purchase_memo: ::protobuf::MessageField<CoinPurchaseMemo>,
// @@protoc_insertion_point(field:hw.trezor.messages.common.PaymentRequest.PaymentRequestMemo.text_details_memo)
pub text_details_memo: ::protobuf::MessageField<TextDetailsMemo>,
// 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::<PaymentRequestMemo>(
"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<Self>;
}
// @@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 {
<TextDetailsMemo as ::protobuf::Message>::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::<TextDetailsMemo>(
"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<Self>;
}
// @@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);

View File

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