cashu/nuts/nut18/
secret.rs

1//! Secret types for NUT-18: Payment Requests
2use serde::{Deserialize, Serialize};
3
4use crate::nuts::nut10::Kind;
5use crate::nuts::{Nut10Secret, SpendingConditions};
6
7/// Nut10Secret without nonce for payment requests
8#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Nut10SecretRequest {
10    /// Kind of the spending condition
11    #[serde(rename = "k")]
12    pub kind: Kind,
13    /// Secret data
14    #[serde(rename = "d")]
15    pub data: String,
16    /// Additional data committed to and can be used for feature extensions
17    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
18    pub tags: Option<Vec<Vec<String>>>,
19}
20
21impl Nut10SecretRequest {
22    /// Create a new Nut10SecretRequest
23    pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
24    where
25        S: Into<String>,
26        V: Into<Vec<Vec<String>>>,
27    {
28        Self {
29            kind,
30            data: data.into(),
31            tags: tags.map(|v| v.into()),
32        }
33    }
34}
35
36impl From<Nut10Secret> for Nut10SecretRequest {
37    fn from(secret: Nut10Secret) -> Self {
38        Self {
39            kind: secret.kind(),
40            data: secret.secret_data().data().to_string(),
41            tags: secret.secret_data().tags().cloned(),
42        }
43    }
44}
45
46impl From<Nut10SecretRequest> for Nut10Secret {
47    fn from(value: Nut10SecretRequest) -> Self {
48        Self::new(value.kind, value.data, value.tags)
49    }
50}
51
52impl From<SpendingConditions> for Nut10SecretRequest {
53    fn from(conditions: SpendingConditions) -> Self {
54        match conditions {
55            SpendingConditions::P2PKConditions { data, conditions } => {
56                Self::new(Kind::P2PK, data.to_hex(), conditions)
57            }
58            SpendingConditions::HTLCConditions { data, conditions } => {
59                Self::new(Kind::HTLC, data.to_string(), conditions)
60            }
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_nut10_secret_request_serialization() {
71        let request = Nut10SecretRequest::new(
72            Kind::P2PK,
73            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
74            Some(vec![vec!["key".to_string(), "value".to_string()]]),
75        );
76
77        let json = serde_json::to_string(&request).unwrap();
78
79        // Verify json has abbreviated field names
80        assert!(json.contains(r#""k":"P2PK""#));
81        assert!(json.contains(r#""d":"026562"#));
82        assert!(json.contains(r#""t":[["key","#));
83    }
84
85    #[test]
86    fn test_roundtrip_serialization() {
87        let original = Nut10SecretRequest {
88            kind: Kind::P2PK,
89            data: "test_data".into(),
90            tags: Some(vec![vec!["key".to_string(), "value".to_string()]]),
91        };
92
93        let json = serde_json::to_string(&original).unwrap();
94        let decoded: Nut10SecretRequest = serde_json::from_str(&json).unwrap();
95
96        assert_eq!(original, decoded);
97    }
98
99    #[test]
100    fn test_from_nut10_secret() {
101        let secret = Nut10Secret::new(
102            Kind::P2PK,
103            "test_data",
104            Some(vec![vec!["key".to_string(), "value".to_string()]]),
105        );
106
107        let request: Nut10SecretRequest = secret.clone().into();
108
109        assert_eq!(request.kind, secret.kind());
110        assert_eq!(request.data, secret.secret_data().data());
111        assert_eq!(request.tags, secret.secret_data().tags().cloned());
112    }
113
114    #[test]
115    fn test_into_nut10_secret() {
116        let request = Nut10SecretRequest {
117            kind: Kind::HTLC,
118            data: "test_hash".into(),
119            tags: None,
120        };
121
122        let secret: Nut10Secret = request.clone().into();
123
124        assert_eq!(secret.kind(), request.kind);
125        assert_eq!(secret.secret_data().data(), request.data);
126        assert_eq!(secret.secret_data().tags(), request.tags.as_ref());
127    }
128}