Skip to main content

cashu/nuts/nut18/
secret.rs

1//! Secret types for NUT-18: Payment Requests
2use serde::{Deserialize, Serialize};
3
4use crate::nuts::nut10::{Error as Nut10Error, Kind, SpendingConditions};
5use crate::nuts::Nut10Secret;
6use crate::SecretData;
7
8/// Nut10Secret without nonce for payment requests
9#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Nut10SecretRequest {
11    /// Kind of the spending condition
12    #[serde(rename = "k")]
13    pub kind: Kind,
14    /// Secret data
15    #[serde(rename = "d")]
16    pub data: String,
17    /// Additional data committed to and can be used for feature extensions
18    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
19    pub tags: Option<Vec<Vec<String>>>,
20}
21
22impl Nut10SecretRequest {
23    /// Create a new Nut10SecretRequest
24    pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
25    where
26        S: Into<String>,
27        V: Into<Vec<Vec<String>>>,
28    {
29        Self {
30            kind,
31            data: data.into(),
32            tags: tags.map(Into::into),
33        }
34    }
35}
36
37impl From<Nut10Secret> for Nut10SecretRequest {
38    fn from(secret: Nut10Secret) -> Self {
39        Self {
40            kind: secret.kind(),
41            data: secret.secret_data().data().to_string(),
42            tags: secret.secret_data().tags().cloned(),
43        }
44    }
45}
46
47impl From<Nut10SecretRequest> for Nut10Secret {
48    fn from(value: Nut10SecretRequest) -> Self {
49        Self::new(value.kind, SecretData::new(value.data, value.tags))
50    }
51}
52
53/// A request advertises a lock the payer will build, so it has to pass the same
54/// construction rules the secret itself would.
55impl TryFrom<SpendingConditions> for Nut10SecretRequest {
56    type Error = Nut10Error;
57    fn try_from(conditions: SpendingConditions) -> Result<Self, Self::Error> {
58        conditions.validate()?;
59
60        Ok(match conditions {
61            SpendingConditions::P2PKConditions { data, conditions } => {
62                Self::new(Kind::P2PK, data.to_hex(), conditions)
63            }
64            SpendingConditions::HTLCConditions { data, conditions } => {
65                Self::new(Kind::HTLC, data.to_string(), conditions)
66            }
67        })
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_nut10_secret_request_serialization() {
77        let request = Nut10SecretRequest::new(
78            Kind::P2PK,
79            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
80            Some(vec![vec!["key".to_string(), "value".to_string()]]),
81        );
82
83        let json = serde_json::to_string(&request).unwrap();
84
85        // Verify json has abbreviated field names
86        assert!(json.contains(r#""k":"P2PK""#));
87        assert!(json.contains(r#""d":"026562"#));
88        assert!(json.contains(r#""t":[["key","#));
89    }
90
91    #[test]
92    fn test_roundtrip_serialization() {
93        let original = Nut10SecretRequest {
94            kind: Kind::P2PK,
95            data: "test_data".into(),
96            tags: Some(vec![vec!["key".to_string(), "value".to_string()]]),
97        };
98
99        let json = serde_json::to_string(&original).unwrap();
100        let decoded: Nut10SecretRequest = serde_json::from_str(&json).unwrap();
101
102        assert_eq!(original, decoded);
103    }
104
105    /// A request advertises a lock the payer will go on to build, so it cannot
106    /// carry one the payer would refuse.
107    #[test]
108    fn test_request_rejects_duplicate_p2pk_key() {
109        use std::str::FromStr;
110
111        use crate::nuts::nut10::Conditions;
112        use crate::nuts::nut11::Error as Nut11Error;
113        use crate::PublicKey;
114
115        let key = PublicKey::from_str(
116            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
117        )
118        .unwrap();
119
120        let result = Nut10SecretRequest::try_from(SpendingConditions::P2PKConditions {
121            data: key,
122            conditions: Some(Conditions {
123                pubkeys: Some(vec![key]),
124                ..Default::default()
125            }),
126        });
127
128        assert!(matches!(
129            result,
130            Err(Nut10Error::NUT11(Nut11Error::DuplicatePubkey))
131        ));
132    }
133
134    #[test]
135    fn test_from_nut10_secret() {
136        let secret = Nut10Secret::new(
137            Kind::P2PK,
138            SecretData::new(
139                "test_data",
140                Some(vec![vec!["key".to_string(), "value".to_string()]]),
141            ),
142        );
143
144        let request: Nut10SecretRequest = secret.clone().into();
145
146        assert_eq!(request.kind, secret.kind());
147        assert_eq!(request.data, secret.secret_data().data());
148        assert_eq!(request.tags, secret.secret_data().tags().cloned());
149    }
150
151    #[test]
152    fn test_into_nut10_secret() {
153        let request = Nut10SecretRequest {
154            kind: Kind::HTLC,
155            data: "test_hash".into(),
156            tags: None,
157        };
158
159        let secret: Nut10Secret = request.clone().into();
160
161        assert_eq!(secret.kind(), request.kind);
162        assert_eq!(secret.secret_data().data(), request.data);
163        assert_eq!(secret.secret_data().tags(), request.tags.as_ref());
164    }
165}