openzeppelin_relayer/models/transaction/stellar/
memo.rs1use std::convert::TryFrom;
4
5use serde::{Deserialize, Serialize};
6use soroban_rs::xdr::{Hash, Memo, StringM};
7use utoipa::ToSchema;
8
9use crate::models::SignerError;
10use crate::utils::{deserialize_u64, serialize_u64};
11
12#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, ToSchema)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum MemoSpec {
15 None,
16 Text {
17 value: String,
18 }, Id {
20 #[serde(serialize_with = "serialize_u64", deserialize_with = "deserialize_u64")]
25 #[schema(value_type = String, pattern = "^[0-9]+$")]
26 value: u64,
27 },
28 Hash {
29 #[serde(with = "hex::serde")]
30 value: [u8; 32],
31 },
32 Return {
33 #[serde(with = "hex::serde")]
34 value: [u8; 32],
35 },
36}
37
38impl TryFrom<MemoSpec> for Memo {
39 type Error = SignerError;
40 fn try_from(m: MemoSpec) -> Result<Self, Self::Error> {
41 Ok(match m {
42 MemoSpec::None => Memo::None,
43 MemoSpec::Text { value } => {
44 let text = StringM::<28>::try_from(value.as_str())
45 .map_err(|e| SignerError::ConversionError(format!("Invalid memo text: {e}")))?;
46 Memo::Text(text)
47 }
48 MemoSpec::Id { value } => Memo::Id(value),
49 MemoSpec::Hash { value } => Memo::Hash(Hash(value)),
50 MemoSpec::Return { value } => Memo::Return(Hash(value)),
51 })
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
62 fn test_memo_id_serializes_as_string() {
63 let memo = MemoSpec::Id {
64 value: 18446744073709551615,
65 };
66 let json = serde_json::to_string(&memo).unwrap();
67 assert!(
68 json.contains(r#""value":"18446744073709551615""#),
69 "memo id should serialize as a string, got: {json}"
70 );
71 }
72
73 #[test]
74 fn test_memo_id_deserializes_from_corrupted_float() {
75 let mut value = serde_json::to_value(MemoSpec::Id {
76 value: 1099511627776,
77 })
78 .unwrap();
79 value["value"] = serde_json::json!(1099511627776.0);
80 let json = serde_json::to_string(&value).unwrap();
81
82 let parsed: MemoSpec = serde_json::from_str(&json).unwrap();
83 match parsed {
84 MemoSpec::Id { value } => assert_eq!(value, 1099511627776),
85 other => panic!("expected Id, got {other:?}"),
86 }
87 }
88
89 #[test]
90 fn test_memo_none() {
91 let spec = MemoSpec::None;
92 let memo = Memo::try_from(spec).unwrap();
93 assert!(matches!(memo, Memo::None));
94 }
95
96 #[test]
97 fn test_memo_text() {
98 let spec = MemoSpec::Text {
99 value: "Hello World".to_string(),
100 };
101 let memo = Memo::try_from(spec).unwrap();
102 assert!(matches!(memo, Memo::Text(_)));
103 }
104
105 #[test]
106 fn test_memo_id() {
107 let spec = MemoSpec::Id { value: 12345 };
108 let memo = Memo::try_from(spec).unwrap();
109 assert!(matches!(memo, Memo::Id(12345)));
110 }
111
112 #[test]
113 fn test_memo_spec_serde() {
114 let spec = MemoSpec::Text {
115 value: "hello".to_string(),
116 };
117 let json = serde_json::to_string(&spec).unwrap();
118 assert!(json.contains("text"));
119 assert!(json.contains("hello"));
120
121 let deserialized: MemoSpec = serde_json::from_str(&json).unwrap();
122 assert_eq!(spec, deserialized);
123 }
124
125 #[test]
126 fn test_memo_spec_json_format() {
127 let none = MemoSpec::None;
129 let none_json = serde_json::to_value(&none).unwrap();
130 assert_eq!(none_json, serde_json::json!({"type": "none"}));
131
132 let text = MemoSpec::Text {
134 value: "hello".to_string(),
135 };
136 let text_json = serde_json::to_value(&text).unwrap();
137 assert_eq!(
138 text_json,
139 serde_json::json!({"type": "text", "value": "hello"})
140 );
141
142 let id = MemoSpec::Id { value: 12345 };
146 let id_json = serde_json::to_value(&id).unwrap();
147 assert_eq!(id_json, serde_json::json!({"type": "id", "value": "12345"}));
148
149 let hash = MemoSpec::Hash { value: [0x42; 32] };
151 let hash_json = serde_json::to_value(&hash).unwrap();
152 assert_eq!(hash_json["type"], "hash");
153 assert!(hash_json["value"].is_string()); let ret = MemoSpec::Return { value: [0x42; 32] };
157 let ret_json = serde_json::to_value(&ret).unwrap();
158 assert_eq!(ret_json["type"], "return");
159 assert!(ret_json["value"].is_string()); }
161}