1use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit};
7use super::nut01::PublicKey;
8use super::nut05::MeltRequest;
9use super::MeltQuoteState;
10#[cfg(feature = "mint")]
11use crate::quote_id::QuoteId;
12use crate::util::serde_helpers::deserialize_empty_string_as_none;
13use crate::{Amount, Proofs};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct MintQuoteOnchainRequest {
20 pub unit: CurrencyUnit,
22 pub pubkey: PublicKey,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(bound = "Q: Serialize + DeserializeOwned")]
34pub struct MintQuoteOnchainResponse<Q> {
35 pub quote: Q,
37 pub request: String,
39 pub unit: CurrencyUnit,
41 pub expiry: Option<u64>,
43 pub pubkey: PublicKey,
45 #[serde(default)]
47 pub amount_paid: Amount,
48 #[serde(default)]
50 pub amount_issued: Amount,
51}
52
53impl<Q: ToString> MintQuoteOnchainResponse<Q> {
54 pub fn to_string_id(&self) -> MintQuoteOnchainResponse<String> {
56 MintQuoteOnchainResponse {
57 quote: self.quote.to_string(),
58 request: self.request.clone(),
59 unit: self.unit.clone(),
60 expiry: self.expiry,
61 pubkey: self.pubkey,
62 amount_paid: self.amount_paid,
63 amount_issued: self.amount_issued,
64 }
65 }
66}
67
68#[cfg(feature = "mint")]
69impl From<MintQuoteOnchainResponse<QuoteId>> for MintQuoteOnchainResponse<String> {
70 fn from(value: MintQuoteOnchainResponse<QuoteId>) -> Self {
71 Self {
72 quote: value.quote.to_string(),
73 request: value.request,
74 unit: value.unit,
75 expiry: value.expiry,
76 pubkey: value.pubkey,
77 amount_paid: value.amount_paid,
78 amount_issued: value.amount_issued,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct MeltQuoteOnchainRequest {
88 pub request: String,
90 pub unit: CurrencyUnit,
92 pub amount: Amount,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(bound = "Q: Serialize + DeserializeOwned")]
102pub struct MeltOnchainRequest<Q> {
103 pub quote: Q,
105 pub fee_index: u32,
107 pub inputs: Proofs,
109 pub outputs: Option<Vec<BlindedMessage>>,
111}
112
113impl<Q> From<MeltOnchainRequest<Q>> for MeltRequest<Q>
114where
115 Q: Serialize + DeserializeOwned,
116{
117 fn from(request: MeltOnchainRequest<Q>) -> Self {
118 MeltRequest::new(request.quote, request.inputs, request.outputs)
119 .fee_index(request.fee_index)
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct MeltQuoteOnchainFeeOption {
137 pub fee_index: u32,
139 pub fee_reserve: Amount,
141 pub estimated_blocks: u32,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(bound = "Q: Serialize + DeserializeOwned")]
155pub struct MeltQuoteOnchainResponse<Q> {
156 pub quote: Q,
158 pub amount: Amount,
160 pub unit: CurrencyUnit,
162 pub state: MeltQuoteState,
164 pub expiry: u64,
166 pub request: String,
168 pub fee_options: Vec<MeltQuoteOnchainFeeOption>,
175 pub selected_fee_index: Option<u32>,
177 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
179 pub outpoint: Option<String>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub change: Option<Vec<BlindSignature>>,
183}
184
185impl<Q: ToString> MeltQuoteOnchainResponse<Q> {
186 pub fn to_string_id(&self) -> MeltQuoteOnchainResponse<String> {
188 MeltQuoteOnchainResponse {
189 quote: self.quote.to_string(),
190 amount: self.amount,
191 unit: self.unit.clone(),
192 state: self.state,
193 expiry: self.expiry,
194 request: self.request.clone(),
195 fee_options: self.fee_options.clone(),
196 selected_fee_index: self.selected_fee_index,
197 outpoint: self.outpoint.clone(),
198 change: self.change.clone(),
199 }
200 }
201}
202
203#[cfg(feature = "mint")]
204impl From<MeltQuoteOnchainResponse<QuoteId>> for MeltQuoteOnchainResponse<String> {
205 fn from(value: MeltQuoteOnchainResponse<QuoteId>) -> Self {
206 Self {
207 quote: value.quote.to_string(),
208 amount: value.amount,
209 unit: value.unit,
210 state: value.state,
211 expiry: value.expiry,
212 request: value.request,
213 fee_options: value.fee_options,
214 selected_fee_index: value.selected_fee_index,
215 outpoint: value.outpoint,
216 change: value.change,
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_mint_quote_onchain_request_serialization() {
227 let request = MintQuoteOnchainRequest {
228 unit: CurrencyUnit::Sat,
229 pubkey: PublicKey::from_hex(
230 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
231 )
232 .unwrap(),
233 };
234
235 let serialized = serde_json::to_string(&request).unwrap();
236 let deserialized: MintQuoteOnchainRequest = serde_json::from_str(&serialized).unwrap();
237
238 assert_eq!(request.unit, deserialized.unit);
239 assert_eq!(request.pubkey, deserialized.pubkey);
240 }
241
242 #[test]
243 fn test_melt_quote_onchain_request_serialization() {
244 let request = MeltQuoteOnchainRequest {
245 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
246 unit: CurrencyUnit::Sat,
247 amount: Amount::from(1000),
248 };
249
250 let serialized = serde_json::to_string(&request).unwrap();
251 let deserialized: MeltQuoteOnchainRequest = serde_json::from_str(&serialized).unwrap();
252
253 assert_eq!(request.request, deserialized.request);
254 assert_eq!(request.unit, deserialized.unit);
255 assert_eq!(request.amount, deserialized.amount);
256 }
257
258 #[test]
259 fn test_melt_quote_onchain_response_serialization() {
260 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
261 quote: "TRmjduhIsPxd...".to_string(),
262 amount: Amount::from(100000),
263 unit: CurrencyUnit::Sat,
264 state: MeltQuoteState::Pending,
265 expiry: 1701704757,
266 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
267 fee_options: vec![MeltQuoteOnchainFeeOption {
268 fee_index: 0,
269 fee_reserve: Amount::from(5000),
270 estimated_blocks: 1,
271 }],
272 selected_fee_index: Some(0),
273 outpoint: Some(
274 "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2".to_string(),
275 ),
276 change: None,
277 };
278
279 let serialized = serde_json::to_string(&response).unwrap();
280 assert!(serialized.contains("\"fee_reserve\""));
281 assert!(serialized.contains("\"fee_index\""));
282 assert!(!serialized.contains("\"fee\":"));
283
284 let deserialized: MeltQuoteOnchainResponse<String> =
285 serde_json::from_str(&serialized).unwrap();
286
287 assert_eq!(response.quote, deserialized.quote);
288 assert_eq!(response.request, deserialized.request);
289 assert_eq!(response.amount, deserialized.amount);
290 assert_eq!(response.fee_options, deserialized.fee_options);
291 assert_eq!(response.selected_fee_index, deserialized.selected_fee_index);
292 assert_eq!(response.state, deserialized.state);
293 assert_eq!(response.outpoint, deserialized.outpoint);
294 assert_eq!(response.change, deserialized.change);
295 }
296
297 #[test]
298 fn test_melt_quote_onchain_response_serializes_null_outpoint() {
299 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
300 quote: "TRmjduhIsPxd...".to_string(),
301 amount: Amount::from(100000),
302 unit: CurrencyUnit::Sat,
303 state: MeltQuoteState::Pending,
304 expiry: 1701704757,
305 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
306 fee_options: vec![MeltQuoteOnchainFeeOption {
307 fee_index: 0,
308 fee_reserve: Amount::from(5000),
309 estimated_blocks: 1,
310 }],
311 selected_fee_index: None,
312 outpoint: None,
313 change: None,
314 };
315
316 let serialized = serde_json::to_string(&response).unwrap();
317 assert!(serialized.contains("\"outpoint\":null"));
318
319 let deserialized: MeltQuoteOnchainResponse<String> =
320 serde_json::from_str(&serialized).unwrap();
321 assert_eq!(deserialized.outpoint, None);
322 }
323
324 #[test]
325 fn test_mint_quote_onchain_response_tolerates_unknown_fields() {
326 let encoded = r#"{
329 "quote": "DSGLX9kevM...",
330 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
331 "unit": "sat",
332 "expiry": 1701704757,
333 "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
334 "amount_paid": 100000,
335 "amount_issued": 0,
336 "payjoin": {"endpoint": "https://payjoin.example/pj"}
337 }"#;
338
339 let deserialized: MintQuoteOnchainResponse<String> = serde_json::from_str(encoded).unwrap();
340 assert_eq!(deserialized.quote, "DSGLX9kevM...");
341 assert_eq!(deserialized.amount_paid, Amount::from(100000));
342 }
343
344 #[test]
345 fn test_melt_quote_onchain_response_tolerates_unknown_fields() {
346 let encoded = r#"{
347 "quote": "TRmjduhIsPxd...",
348 "amount": 100000,
349 "unit": "sat",
350 "state": "PENDING",
351 "expiry": 1701704757,
352 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
353 "fee_options": [{"fee_index": 0, "fee_reserve": 5000, "estimated_blocks": 1}],
354 "selected_fee_index": null,
355 "outpoint": null,
356 "payjoin": {"endpoint": "https://payjoin.example/pj"}
357 }"#;
358
359 let deserialized: MeltQuoteOnchainResponse<String> = serde_json::from_str(encoded).unwrap();
360 assert_eq!(deserialized.quote, "TRmjduhIsPxd...");
361 assert_eq!(deserialized.state, MeltQuoteState::Pending);
362 }
363
364 #[test]
365 fn test_mint_quote_onchain_response_to_string_id() {
366 use crate::nuts::nut00::CurrencyUnit;
367 use crate::nuts::nut01::PublicKey;
368 use crate::Amount;
369
370 let response: MintQuoteOnchainResponse<String> = MintQuoteOnchainResponse {
371 quote: "DSGLX9kevM...".to_string(),
372 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
373 unit: CurrencyUnit::Sat,
374 expiry: Some(1701704757),
375 pubkey: PublicKey::from_hex(
376 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
377 )
378 .unwrap(),
379 amount_paid: Amount::from(100000),
380 amount_issued: Amount::from(0),
381 };
382
383 let string_id_response = response.to_string_id();
384 assert_eq!(string_id_response.quote, "DSGLX9kevM...");
385 }
386
387 #[test]
388 fn test_melt_quote_onchain_response_to_string_id() {
389 use crate::nuts::nut00::CurrencyUnit;
390 use crate::Amount;
391
392 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
393 quote: "TRmjduhIsPxd...".to_string(),
394 amount: Amount::from(100000),
395 unit: CurrencyUnit::Sat,
396 state: MeltQuoteState::Pending,
397 expiry: 1701704757,
398 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
399 fee_options: vec![MeltQuoteOnchainFeeOption {
400 fee_index: 0,
401 fee_reserve: Amount::from(5000),
402 estimated_blocks: 1,
403 }],
404 selected_fee_index: Some(0),
405 outpoint: Some(
406 "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2".to_string(),
407 ),
408 change: None,
409 };
410
411 let string_id_response = response.to_string_id();
412 assert_eq!(string_id_response.quote, "TRmjduhIsPxd...");
413 }
414}