openzeppelin_relayer/models/transaction/stellar/
memo.rs

1//! Memo types and conversions for Stellar transactions
2
3use crate::models::SignerError;
4use serde::{Deserialize, Serialize};
5use soroban_rs::xdr::{Hash, Memo, StringM};
6use std::convert::TryFrom;
7use utoipa::ToSchema;
8
9#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, ToSchema)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum MemoSpec {
12    None,
13    Text {
14        value: String,
15    }, // ≤ 28 UTF-8 bytes
16    Id {
17        value: u64,
18    },
19    Hash {
20        #[serde(with = "hex::serde")]
21        value: [u8; 32],
22    },
23    Return {
24        #[serde(with = "hex::serde")]
25        value: [u8; 32],
26    },
27}
28
29impl TryFrom<MemoSpec> for Memo {
30    type Error = SignerError;
31    fn try_from(m: MemoSpec) -> Result<Self, Self::Error> {
32        Ok(match m {
33            MemoSpec::None => Memo::None,
34            MemoSpec::Text { value } => {
35                let text = StringM::<28>::try_from(value.as_str()).map_err(|e| {
36                    SignerError::ConversionError(format!("Invalid memo text: {}", e))
37                })?;
38                Memo::Text(text)
39            }
40            MemoSpec::Id { value } => Memo::Id(value),
41            MemoSpec::Hash { value } => Memo::Hash(Hash(value)),
42            MemoSpec::Return { value } => Memo::Return(Hash(value)),
43        })
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn test_memo_none() {
53        let spec = MemoSpec::None;
54        let memo = Memo::try_from(spec).unwrap();
55        assert!(matches!(memo, Memo::None));
56    }
57
58    #[test]
59    fn test_memo_text() {
60        let spec = MemoSpec::Text {
61            value: "Hello World".to_string(),
62        };
63        let memo = Memo::try_from(spec).unwrap();
64        assert!(matches!(memo, Memo::Text(_)));
65    }
66
67    #[test]
68    fn test_memo_id() {
69        let spec = MemoSpec::Id { value: 12345 };
70        let memo = Memo::try_from(spec).unwrap();
71        assert!(matches!(memo, Memo::Id(12345)));
72    }
73
74    #[test]
75    fn test_memo_spec_serde() {
76        let spec = MemoSpec::Text {
77            value: "hello".to_string(),
78        };
79        let json = serde_json::to_string(&spec).unwrap();
80        assert!(json.contains("text"));
81        assert!(json.contains("hello"));
82
83        let deserialized: MemoSpec = serde_json::from_str(&json).unwrap();
84        assert_eq!(spec, deserialized);
85    }
86
87    #[test]
88    fn test_memo_spec_json_format() {
89        // Test None
90        let none = MemoSpec::None;
91        let none_json = serde_json::to_value(&none).unwrap();
92        assert_eq!(none_json, serde_json::json!({"type": "none"}));
93
94        // Test Text
95        let text = MemoSpec::Text {
96            value: "hello".to_string(),
97        };
98        let text_json = serde_json::to_value(&text).unwrap();
99        assert_eq!(
100            text_json,
101            serde_json::json!({"type": "text", "value": "hello"})
102        );
103
104        // Test Id
105        let id = MemoSpec::Id { value: 12345 };
106        let id_json = serde_json::to_value(&id).unwrap();
107        assert_eq!(id_json, serde_json::json!({"type": "id", "value": 12345}));
108
109        // Test Hash
110        let hash = MemoSpec::Hash { value: [0x42; 32] };
111        let hash_json = serde_json::to_value(&hash).unwrap();
112        assert_eq!(hash_json["type"], "hash");
113        assert!(hash_json["value"].is_string()); // hex encoded
114
115        // Test Return
116        let ret = MemoSpec::Return { value: [0x42; 32] };
117        let ret_json = serde_json::to_value(&ret).unwrap();
118        assert_eq!(ret_json["type"], "return");
119        assert!(ret_json["value"].is_string()); // hex encoded
120    }
121}