Skip to main content

cashu/nuts/
nut03.rs

1//! NUT-03: Swap
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/03.md>
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[cfg(feature = "wallet")]
9use super::nut00::PreMintSecrets;
10use super::nut00::{BlindSignature, BlindedMessage, Proofs};
11use super::ProofsMethods;
12use crate::Amount;
13
14/// NUT03 Error
15#[derive(Debug, Error)]
16pub enum Error {
17    /// DHKE error
18    #[error(transparent)]
19    DHKE(#[from] crate::dhke::Error),
20    /// Amount Error
21    #[error(transparent)]
22    Amount(#[from] crate::amount::Error),
23}
24
25/// Preswap information
26#[cfg(feature = "wallet")]
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct PreSwap {
29    /// Preswap mint secrets
30    pub pre_mint_secrets: PreMintSecrets,
31    /// Swap request
32    pub swap_request: SwapRequest,
33    /// Amount to increment keyset counter by
34    pub derived_secret_count: u32,
35    /// Fee amount
36    pub fee: Amount,
37    /// Ephemeral secret keys used for p2bk
38    pub p2bk_secret_keys: Option<Vec<crate::nuts::nut01::SecretKey>>,
39}
40
41/// Swap Request [NUT-03]
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct SwapRequest {
44    /// Proofs that are to be spent in a `Swap`
45    inputs: Proofs,
46    /// Blinded Messages for Mint to sign
47    outputs: Vec<BlindedMessage>,
48}
49
50impl SwapRequest {
51    /// Create new [`SwapRequest`]
52    pub fn new(inputs: Proofs, outputs: Vec<BlindedMessage>) -> Self {
53        Self {
54            inputs: inputs.without_dleqs(),
55            outputs,
56        }
57    }
58
59    /// Get inputs (proofs)
60    pub fn inputs(&self) -> &Proofs {
61        &self.inputs
62    }
63
64    /// Get mutable inputs (proofs)
65    pub fn inputs_mut(&mut self) -> &mut Proofs {
66        &mut self.inputs
67    }
68
69    /// Get outputs (blinded messages)
70    pub fn outputs(&self) -> &Vec<BlindedMessage> {
71        &self.outputs
72    }
73
74    /// Get mutable reference to outputs (blinded messages)
75    pub fn outputs_mut(&mut self) -> &mut Vec<BlindedMessage> {
76        &mut self.outputs
77    }
78
79    /// Total value of proofs in [`SwapRequest`]
80    pub fn input_amount(&self) -> Result<Amount, Error> {
81        Ok(Amount::try_sum(
82            self.inputs.iter().map(|proof| proof.amount),
83        )?)
84    }
85
86    /// Total value of outputs in [`SwapRequest`]
87    pub fn output_amount(&self) -> Result<Amount, Error> {
88        Ok(Amount::try_sum(
89            self.outputs.iter().map(|proof| proof.amount),
90        )?)
91    }
92}
93
94impl super::nut10::SpendingConditionVerification for SwapRequest {
95    fn inputs(&self) -> &Proofs {
96        &self.inputs
97    }
98
99    fn sig_all_msg_to_sign(&self) -> String {
100        let mut msg = String::new();
101
102        // Add all input secrets and C values in order
103        // msg = secret_0 || C_0 || ... || secret_n || C_n
104        for proof in &self.inputs {
105            msg.push_str(&proof.secret.to_string());
106            msg.push_str(&proof.c.to_hex());
107        }
108
109        // Add all output amounts and B_ values in order
110        // msg = ... || amount_0 || B_0 || ... || amount_m || B_m
111        for output in &self.outputs {
112            msg.push_str(&output.amount.to_string());
113            msg.push_str(&output.blinded_secret.to_hex());
114        }
115
116        msg
117    }
118}
119
120/// Split Response [NUT-06]
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct SwapResponse {
123    /// Promises
124    pub signatures: Vec<BlindSignature>,
125}
126
127impl SwapResponse {
128    /// Create new [`SwapResponse`]
129    pub fn new(promises: Vec<BlindSignature>) -> Self {
130        Self {
131            signatures: promises,
132        }
133    }
134
135    /// Total [`Amount`] of promises
136    pub fn promises_amount(&self) -> Result<Amount, Error> {
137        Ok(Amount::try_sum(
138            self.signatures
139                .iter()
140                .map(|BlindSignature { amount, .. }| *amount),
141        )?)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    const SWAP_REQUEST_JSON: &str = r#"{
150        "inputs": [
151            {
152                "amount": 2,
153                "id": "00bfa73302d12ffd",
154                "secret": "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]",
155                "C": "02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd",
156                "witness": "{\"signatures\":[\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\"]}"
157            },
158            {
159                "amount": 4,
160                "id": "00bfa73302d12ffd",
161                "secret": "[\"P2PK\",{\"nonce\":\"d7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950304\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]",
162                "C": "02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd"
163            }
164        ],
165        "outputs": [
166            {
167                "amount": 2,
168                "id": "00bfa73302d12ffd",
169                "B_": "038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"
170            }
171        ]
172    }"#;
173
174    #[test]
175    fn test_swap_request_inputs_outputs_getters() {
176        // Kills mutations that replace `inputs()` / `outputs()` with empty
177        // leaked boxes by asserting both length and element content.
178        let req: SwapRequest = serde_json::from_str(SWAP_REQUEST_JSON).unwrap();
179
180        let inputs = req.inputs();
181        assert_eq!(inputs.len(), 2, "expected 2 inputs");
182        assert_eq!(u64::from(inputs[0].amount), 2);
183        assert_eq!(u64::from(inputs[1].amount), 4);
184
185        let outputs = req.outputs();
186        assert_eq!(outputs.len(), 1, "expected 1 output");
187        assert_eq!(u64::from(outputs[0].amount), 2);
188    }
189
190    #[test]
191    fn test_swap_request_inputs_outputs_getters_via_new() {
192        // Round-trip through SwapRequest::new to ensure the getters
193        // return the data we constructed the request with, not an empty default.
194        let req: SwapRequest = serde_json::from_str(SWAP_REQUEST_JSON).unwrap();
195        let inputs_clone = req.inputs().clone();
196        let outputs_clone = req.outputs().clone();
197
198        let rebuilt = SwapRequest::new(inputs_clone, outputs_clone);
199        assert_eq!(rebuilt.inputs().len(), 2);
200        assert_eq!(rebuilt.outputs().len(), 1);
201        assert!(!rebuilt.inputs().is_empty());
202        assert!(!rebuilt.outputs().is_empty());
203    }
204
205    #[test]
206    fn test_swap_request_outputs_mut_updates_outputs() {
207        let mut req: SwapRequest = serde_json::from_str(SWAP_REQUEST_JSON).unwrap();
208        let output = req.outputs()[0].clone();
209
210        req.outputs_mut().push(output);
211
212        assert_eq!(req.outputs().len(), 2);
213    }
214
215    #[test]
216    fn test_swap_request_amounts() {
217        let req: SwapRequest = serde_json::from_str(SWAP_REQUEST_JSON).unwrap();
218
219        assert_eq!(req.input_amount().unwrap(), Amount::from(6));
220        assert_eq!(req.output_amount().unwrap(), Amount::from(2));
221    }
222
223    #[test]
224    fn test_swap_response_promises_amount() {
225        let response: SwapResponse = serde_json::from_str(
226            r#"{
227                "signatures": [
228                    {
229                        "amount": 8,
230                        "id": "00bfa73302d12ffd",
231                        "C_": "02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd"
232                    },
233                    {
234                        "amount": 4,
235                        "id": "00bfa73302d12ffd",
236                        "C_": "038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"
237                    }
238                ]
239            }"#,
240        )
241        .unwrap();
242
243        assert_eq!(response.promises_amount().unwrap(), Amount::from(12));
244    }
245}