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())
36                    .map_err(|e| SignerError::ConversionError(format!("Invalid memo text: {e}")))?;
37                Memo::Text(text)
38            }
39            MemoSpec::Id { value } => Memo::Id(value),
40            MemoSpec::Hash { value } => Memo::Hash(Hash(value)),
41            MemoSpec::Return { value } => Memo::Return(Hash(value)),
42        })
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn test_memo_none() {
52        let spec = MemoSpec::None;
53        let memo = Memo::try_from(spec).unwrap();
54        assert!(matches!(memo, Memo::None));
55    }
56
57    #[test]
58    fn test_memo_text() {
59        let spec = MemoSpec::Text {
60            value: "Hello World".to_string(),
61        };
62        let memo = Memo::try_from(spec).unwrap();
63        assert!(matches!(memo, Memo::Text(_)));
64    }
65
66    #[test]
67    fn test_memo_id() {
68        let spec = MemoSpec::Id { value: 12345 };
69        let memo = Memo::try_from(spec).unwrap();
70        assert!(matches!(memo, Memo::Id(12345)));
71    }
72
73    #[test]
74    fn test_memo_spec_serde() {
75        let spec = MemoSpec::Text {
76            value: "hello".to_string(),
77        };
78        let json = serde_json::to_string(&spec).unwrap();
79        assert!(json.contains("text"));
80        assert!(json.contains("hello"));
81
82        let deserialized: MemoSpec = serde_json::from_str(&json).unwrap();
83        assert_eq!(spec, deserialized);
84    }
85
86    #[test]
87    fn test_memo_spec_json_format() {
88        // Test None
89        let none = MemoSpec::None;
90        let none_json = serde_json::to_value(&none).unwrap();
91        assert_eq!(none_json, serde_json::json!({"type": "none"}));
92
93        // Test Text
94        let text = MemoSpec::Text {
95            value: "hello".to_string(),
96        };
97        let text_json = serde_json::to_value(&text).unwrap();
98        assert_eq!(
99            text_json,
100            serde_json::json!({"type": "text", "value": "hello"})
101        );
102
103        // Test Id
104        let id = MemoSpec::Id { value: 12345 };
105        let id_json = serde_json::to_value(&id).unwrap();
106        assert_eq!(id_json, serde_json::json!({"type": "id", "value": 12345}));
107
108        // Test Hash
109        let hash = MemoSpec::Hash { value: [0x42; 32] };
110        let hash_json = serde_json::to_value(&hash).unwrap();
111        assert_eq!(hash_json["type"], "hash");
112        assert!(hash_json["value"].is_string()); // hex encoded
113
114        // Test Return
115        let ret = MemoSpec::Return { value: [0x42; 32] };
116        let ret_json = serde_json::to_value(&ret).unwrap();
117        assert_eq!(ret_json["type"], "return");
118        assert!(ret_json["value"].is_string()); // hex encoded
119    }
120}