stellar_baselib/
memo.rs

1use std::str::FromStr;
2
3use crate::xdr;
4use num_traits::ToPrimitive;
5
6const MEMO_NONE: &str = "none";
7const MEMO_ID: &str = "id";
8const MEMO_TEXT: &str = "text";
9const MEMO_HASH: &str = "hash";
10const MEMO_RETURN: &str = "return";
11
12pub enum MemoValue {
13    NoneValue,
14    IdValue(String),
15    TextValue(Vec<u8>),
16    HashValue(Vec<u8>),
17    ReturnValue(Vec<u8>),
18}
19
20#[derive(Debug)]
21pub struct Memo {
22    memo_type: String,
23    value: Option<String>,
24}
25
26// Define a trait for Memo behavior
27pub trait MemoBehavior {
28    fn new(memo_type: &str, value: Option<&str>) -> Result<Self, Box<dyn std::error::Error>>
29    where
30        Self: Sized;
31    fn id(input: &str) -> Self
32    where
33        Self: Sized;
34    fn text(input: &str) -> Self
35    where
36        Self: Sized;
37    fn text_buffer(input: Vec<u8>) -> Self
38    where
39        Self: Sized;
40    fn hash_buffer(input: Vec<u8>) -> Self
41    where
42        Self: Sized;
43    fn return_hash(input: Vec<u8>) -> Self
44    where
45        Self: Sized;
46    fn none() -> Self
47    where
48        Self: Sized;
49    fn value(&self) -> Result<MemoValue, &'static str>;
50    fn from_xdr_object(object: xdr::Memo) -> Result<Self, &'static str>
51    where
52        Self: Sized;
53    fn to_xdr_object(&self) -> Option<xdr::Memo>;
54    fn _validate_id_value(value: &str) -> Result<(), String>;
55    fn _validate_text_value(value: &str);
56    fn _validate_hash_value(value: &[u8]);
57}
58
59impl MemoBehavior for Memo {
60    fn new(memo_type: &str, value: Option<&str>) -> Result<Self, Box<dyn std::error::Error>> {
61        let mut value_buf = None;
62        match memo_type {
63            MEMO_NONE => {}
64            MEMO_ID => {
65                Self::_validate_id_value(value.expect("Expected a value for MEMO_ID"));
66                if let Some(v) = value {
67                    unsafe {
68                        value_buf = Some(String::from_utf8_unchecked(v.into()));
69                    }
70                }
71            }
72            MEMO_TEXT => {
73                Self::_validate_text_value(value.expect("Expected a value for MEMO_TEXT"));
74                if let Some(v) = value {
75                    unsafe {
76                        value_buf = Some(String::from_utf8_unchecked(v.into()));
77                    }
78                }
79            }
80            MEMO_HASH | MEMO_RETURN => {
81                Self::_validate_hash_value(unsafe {
82                    String::from_utf8_unchecked(value.unwrap().as_bytes().to_vec()).as_bytes()
83                });
84                if let Some(v) = value {
85                    value_buf = Some(v.into());
86                }
87            }
88            _ => return Err("Invalid memo type".into()),
89        }
90
91        Ok(Memo {
92            memo_type: memo_type.to_string(),
93            value: value_buf,
94        })
95    }
96
97    fn _validate_id_value(value: &str) -> Result<(), String> {
98        let error = format!("Expects an int64 as a string. Got {}", value);
99
100        let number = match value.parse::<i64>() {
101            Ok(num) => num,
102            Err(_) => return Err(error.clone()),
103        };
104
105        Ok(())
106    }
107
108    fn _validate_text_value(value: &str) {
109        assert!(value.len() <= 28, "String is longer than 28 bytes");
110        let _ = xdr::Memo::Text(value.try_into().unwrap());
111    }
112
113    fn id(input: &str) -> Self {
114        unsafe {
115            Memo {
116                memo_type: MEMO_ID.to_string(),
117                value: Some(String::from_utf8_unchecked(input.into())),
118            }
119        }
120    }
121
122    fn text(input: &str) -> Self {
123        assert!(input.len() <= 28, "String is longer than 28 bytes");
124
125        unsafe {
126            Memo {
127                memo_type: MEMO_TEXT.to_string(),
128                value: Some(String::from_utf8_unchecked(input.into())),
129            }
130        }
131    }
132
133    fn text_buffer(input: Vec<u8>) -> Self {
134        unsafe {
135            Memo {
136                memo_type: MEMO_TEXT.to_string(),
137                value: Some(String::from_utf8_unchecked(input)),
138            }
139        }
140    }
141
142    fn hash_buffer(input: Vec<u8>) -> Self {
143        Self::_validate_hash_value(unsafe {
144            String::from_utf8_unchecked(input.clone()).as_bytes()
145        });
146
147        unsafe {
148            Memo {
149                memo_type: MEMO_HASH.to_string(),
150                value: Some(String::from_utf8_unchecked(input)),
151            }
152        }
153    }
154
155    fn return_hash(input: Vec<u8>) -> Self {
156        Self::_validate_hash_value(unsafe {
157            String::from_utf8_unchecked(input.clone()).as_bytes()
158        });
159
160        unsafe {
161            Memo {
162                memo_type: MEMO_RETURN.to_string(),
163                value: Some(String::from_utf8_unchecked(input)),
164            }
165        }
166    }
167
168    fn _validate_hash_value(value: &[u8]) {
169        if value.len() == 64 {
170            // Check if it's hex encoded string
171            let hex_str = match std::str::from_utf8(value) {
172                Ok(s) => s,
173                Err(_) => panic!("Expects a 32 byte hash value or hex encoded string"),
174            };
175
176            if hex::decode(hex_str).is_err() {
177                panic!("Expects a 32 byte hash value or hex encoded string");
178            }
179            let decoded = match hex::decode(hex_str) {
180                Ok(d) => d,
181                Err(_) => panic!("Failed to decode hex string: {}", hex_str),
182            };
183            if decoded.len() != 32 {
184                panic!("Expects a 32 byte hash value or hex encoded string");
185            }
186        } else if value.len() != 32 {
187            let s = std::str::from_utf8(value).unwrap_or("<non-UTF8 data>");
188            panic!("Expects a 32 byte hash value or hex encoded string");
189        }
190    }
191
192    fn none() -> Self {
193        Self {
194            memo_type: MEMO_NONE.to_owned(),
195            value: None,
196        }
197    }
198
199    fn value(&self) -> Result<MemoValue, &'static str> {
200        match self.memo_type.as_str() {
201            MEMO_NONE => Ok(MemoValue::NoneValue),
202            MEMO_ID => Ok(MemoValue::IdValue(self.value.clone().unwrap())),
203            MEMO_TEXT => Ok(MemoValue::TextValue(
204                self.value.clone().unwrap().as_bytes().to_vec(),
205            )),
206            MEMO_HASH | MEMO_RETURN => Ok(MemoValue::HashValue(
207                self.value.clone().unwrap().as_bytes().to_vec(),
208            )),
209            _ => Err("Invalid memo type"),
210        }
211    }
212
213    fn from_xdr_object(object: xdr::Memo) -> Result<Self, &'static str> {
214        unsafe {
215            match object {
216                xdr::Memo::None => Ok(Memo {
217                    memo_type: MEMO_NONE.to_owned(),
218                    value: None,
219                }),
220                xdr::Memo::Text(x) => Ok(Memo {
221                    memo_type: MEMO_TEXT.to_owned(),
222                    value: Some(String::from_utf8_unchecked(x.to_vec())),
223                }),
224                xdr::Memo::Id(x) => Ok(Memo {
225                    memo_type: MEMO_ID.to_owned(),
226                    value: Some(x.to_string()),
227                }),
228                xdr::Memo::Hash(x) => Ok(Memo {
229                    memo_type: MEMO_HASH.to_owned(),
230                    value: Some(String::from_utf8_unchecked(x.0.to_vec())),
231                }),
232                xdr::Memo::Return(x) => Ok(Memo {
233                    memo_type: MEMO_RETURN.to_owned(),
234                    value: Some(String::from_utf8_unchecked(x.0.to_vec())),
235                }),
236            }
237        }
238    }
239
240    fn to_xdr_object(&self) -> Option<xdr::Memo> {
241        match self.memo_type.as_str() {
242            MEMO_NONE => Some(xdr::Memo::None),
243            MEMO_ID => Some(xdr::Memo::Id(
244                u64::from_str(self.value.clone().unwrap().as_str()).unwrap(),
245            )),
246            MEMO_TEXT => Some(xdr::Memo::Text(
247                self.value.clone().unwrap().as_str().try_into().unwrap(),
248            )),
249            MEMO_HASH => Some(xdr::Memo::Hash(
250                xdr::Hash::from_str(&hex::encode(self.value.clone().unwrap().as_str())).unwrap(),
251            )),
252            // MemoType::MemoReturn => Some(XDRMemo::memo_return(&self._value)),
253            MEMO_RETURN => Some(xdr::Memo::Return(
254                xdr::Hash::from_str(&hex::encode(self.value.clone().unwrap().as_str())).unwrap(),
255            )),
256            _ => None,
257        }
258    }
259}
260
261fn assert_panic<F: FnOnce(), S: AsRef<str>>(f: F, expected_msg: S) {
262    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
263    match result {
264        Ok(_) => panic!("Function did not panic as expected"),
265        Err(err) => {
266            if let Some(s) = err.downcast_ref::<&str>() {
267                assert!(
268                    s.contains(expected_msg.as_ref()),
269                    "Unexpected panic message. Got: {}",
270                    s
271                );
272            } else {
273                panic!("Unexpected panic type");
274            }
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use crate::memo::MemoBehavior;
282    use crate::xdr;
283    use crate::xdr::WriteXdr;
284    use core::panic;
285
286    use crate::memo::{MEMO_HASH, MEMO_NONE, MEMO_RETURN};
287
288    use super::{assert_panic, Memo, MEMO_ID, MEMO_TEXT};
289
290    #[test]
291    fn constructor_throws_error_when_type_is_invalid() {
292        let result = Memo::new("test", None);
293        assert!(result.is_err());
294        let err_msg = format!("{:?}", result.err().unwrap());
295        assert!(err_msg.contains("Invalid memo type"));
296    }
297
298    #[test]
299    fn memo_none_converts_to_from_xdr() {
300        let memo = Memo::none().to_xdr_object().unwrap();
301        let base_memo = Memo::from_xdr_object(memo).unwrap();
302        assert_eq!(base_memo.memo_type, MEMO_NONE);
303        assert!(base_memo.value.is_none());
304    }
305
306    #[test]
307    fn memo_text_returns_value_for_correct_argument() {
308        let _ = Memo::new(MEMO_TEXT, Some("test"));
309
310        let memo_utf8 = Memo::new(MEMO_TEXT, Some("三代之時")).unwrap();
311        let val = match memo_utf8.to_xdr_object().unwrap() {
312            xdr::Memo::Text(x) => x.to_utf8_string().unwrap(),
313
314            _ => panic!("Invalid Type"),
315        };
316        let b = String::from("三代之時");
317        print!("xx {}", val);
318
319        assert_eq!(val, b, "Memo text value does not match expected value");
320    }
321
322    #[test]
323    fn returns_value_for_correct_argument_utf8() {
324        let vec2: Vec<u8> = vec![0xd1];
325        let expected: Vec<u8> = vec![
326            // memo_text
327            0x00, 0x00, 0x00, 0x01, // memo_text
328            0x00, 0x00, 0x00, 0x01, // length
329            0xd1, 0x00, 0x00, 0x00,
330        ];
331        // let mut memo_text: Vec<u8> = vec![];
332        let memo_text = Memo::text_buffer(vec2.clone())
333            .to_xdr_object()
334            .unwrap()
335            .to_xdr(xdr::Limits::none())
336            .unwrap();
337
338        unsafe {
339            let memo_text_2 =
340                Memo::new(MEMO_TEXT, Some(&String::from_utf8_unchecked(vec2.clone())))
341                    .unwrap()
342                    .to_xdr_object()
343                    .unwrap()
344                    .to_xdr(xdr::Limits::none())
345                    .unwrap();
346            assert_eq!(memo_text_2, expected);
347        }
348        assert_eq!(memo_text, expected);
349    }
350
351    #[test]
352    fn converts_to_from_xdr_object() {
353        let memo = Memo::text("test").to_xdr_object().unwrap();
354
355        let val = match memo.clone() {
356            xdr::Memo::Text(x) => x.to_string(),
357            _ => panic!("Invalid Type"),
358        };
359
360        assert_eq!(val, "test");
361
362        let base_memo = Memo::from_xdr_object(memo.clone()).unwrap();
363        assert_eq!(base_memo.memo_type, MEMO_TEXT);
364        assert_eq!(base_memo.value.unwrap(), "test");
365    }
366
367    #[test]
368    fn converts_to_from_xdr_object_buffer() {
369        let buf = vec![0xd1];
370        // unsafe {
371        let memo = Memo::text_buffer(buf.clone()).to_xdr_object().unwrap();
372        // }
373        let val = match memo.clone() {
374            xdr::Memo::Text(x) => x,
375            _ => panic!("Invalid Type"),
376        };
377
378        unsafe {
379            assert_eq!(val.to_vec(), buf);
380        }
381
382        let base_memo = Memo::from_xdr_object(memo).unwrap();
383        assert_eq!(base_memo.memo_type, MEMO_TEXT);
384
385        let val = match base_memo.value().unwrap() {
386            crate::memo::MemoValue::TextValue(x) => x,
387            _ => panic!("Bad"),
388        };
389        unsafe {
390            assert_eq!(val.to_vec(), buf);
391        }
392    }
393
394    #[test]
395    fn errors_when_string_longer_than_28_bytes() {
396        let long_string = "12345678901234567890123456789";
397        let scenario_1 = || {
398            Memo::text(long_string);
399        };
400        assert_panic(scenario_1, "String is longer than 28 bytes");
401
402        let scenario_2 = || {
403            let long_utf8_string = "三代之時三代之時三代之時";
404            Memo::text(long_utf8_string);
405        };
406        assert_panic(scenario_2, "String is longer than 28 bytes");
407    }
408
409    fn memo_id_handles_correct_argument() {
410        Memo::new(MEMO_ID, Some("1000"));
411        Memo::new(MEMO_ID, Some("0"));
412    }
413
414    #[test]
415    fn converts_to_from_xdr_object_if() {
416        let memo = Memo::id("1000").to_xdr_object().unwrap();
417
418        let val = match memo {
419            xdr::Memo::Id(x) => x,
420            _ => panic!("Invalid Type"),
421        };
422
423        assert_eq!(val.to_string(), "1000");
424
425        let base_memo = Memo::from_xdr_object(memo).unwrap();
426
427        match base_memo.memo_type.as_str() {
428            MEMO_ID => (),
429            _ => panic!("Invalid"),
430        }
431
432        assert_eq!(base_memo.value.unwrap(), "1000");
433    }
434
435    #[test]
436    fn hash_converts_to_from_xdr_object() {
437        // Assuming you have a Rust-equivalent to allocate a buffer of length 32 with all bytes being 10.
438        let buffer = vec![10u8; 32];
439
440        let memo = Memo::hash_buffer(buffer.clone()).to_xdr_object().unwrap();
441
442        let val = match memo.clone() {
443            xdr::Memo::Hash(x) => x,
444            _ => panic!("Invalid"),
445        };
446        assert_eq!(val.0.len(), 32);
447        unsafe {
448            assert_eq!(
449                val.to_string(),
450                String::from_utf8_unchecked(hex::encode(buffer.clone()).into())
451            );
452        }
453        let base_memo = Memo::from_xdr_object(memo).unwrap();
454
455        match base_memo.memo_type.as_str() {
456            MEMO_HASH => (),
457            _ => panic!("Invalid"),
458        }
459        assert_eq!(base_memo.value.clone().unwrap().len(), 32);
460
461        let base_memo_hex = hex::encode(base_memo.value.unwrap());
462        let buffer_hex = hex::encode(buffer.clone());
463
464        assert_eq!(base_memo_hex, buffer_hex);
465    }
466
467    #[test]
468    fn return_converts_to_from_xdr_object() {
469        let buffer = vec![10u8; 32];
470
471        // Convert Vec<u8> to hex string
472        let buffer_hex: String = hex::encode(&buffer);
473
474        // Testing string hash
475        let memo = Memo::return_hash(unsafe { buffer.clone() })
476            .to_xdr_object()
477            .unwrap();
478
479        let val = match memo.clone() {
480            xdr::Memo::Return(x) => x,
481            _ => panic!("Invalid"),
482        };
483
484        assert_eq!(val.0.len(), 32);
485        unsafe {
486            assert_eq!(
487                val.to_string(),
488                String::from_utf8_unchecked(hex::encode(buffer.clone()).into())
489            );
490        }
491
492        let base_memo = Memo::from_xdr_object(memo).unwrap();
493
494        match base_memo.memo_type.as_str() {
495            MEMO_RETURN => (),
496            _ => panic!("Invalid"),
497        };
498
499        assert_eq!(base_memo.value.clone().unwrap().len(), 32);
500        let base_memo_hex = hex::encode(base_memo.value.unwrap());
501        let buffer_hex = hex::encode(buffer.clone());
502        assert_eq!(base_memo_hex, buffer_hex);
503    }
504
505    #[test]
506    fn returns_value_for_correct_argument() {
507        let methods = [Memo::hash_buffer, Memo::return_hash];
508
509        for method in &methods {
510            let _ = method(vec![0u8; 32]);
511
512            let hex_str = "0000000000000000000000000000000000000000000000000000000000000000";
513            let _ = method(hex::decode(hex_str).expect("Failed to decode hex"));
514        }
515
516        let binding_1 =
517            hex::decode("00000000000000000000000000000000000000000000000000000000000000").unwrap();
518        let binding_2 =
519            hex::decode("000000000000000000000000000000000000000000000000000000000000000000")
520                .unwrap();
521        let binding_3 = &vec![0u8; 33][..];
522
523        let invalid_inputs = vec![
524            &[] as &[u8], // empty
525            &b"test"[..], // "test" as bytes
526            &[0, 10, 20],
527            binding_3,      // 33 zeros
528            &binding_1[..], // 31 zeros in hex
529            &binding_2[..], // 32 zeros in hex
530                            // ... add any other byte slices as needed
531        ];
532
533        for method in &methods {
534            for input in &invalid_inputs {
535                let scenario_1 = || {
536                    method(input.to_vec());
537                };
538                assert_panic(
539                    scenario_1,
540                    "Expects a 32 byte hash value or hex encoded string",
541                );
542            }
543        }
544    }
545}