1use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, KnownMethod, PaymentMethod};
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
15fn default_onchain_method() -> PaymentMethod {
16 PaymentMethod::Known(KnownMethod::Onchain)
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct MintQuoteOnchainRequest {
24 pub unit: CurrencyUnit,
26 pub pubkey: PublicKey,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(bound = "Q: Serialize + DeserializeOwned")]
38pub struct MintQuoteOnchainResponse<Q> {
39 pub quote: Q,
41 pub request: String,
43 pub unit: CurrencyUnit,
45 #[serde(default = "default_onchain_method")]
47 pub method: PaymentMethod,
48 pub expiry: Option<u64>,
50 pub pubkey: PublicKey,
52 #[serde(default)]
54 pub amount_paid: Amount,
55 #[serde(default)]
57 pub amount_issued: Amount,
58 #[serde(default)]
60 pub updated_at: u64,
61}
62
63impl<Q: ToString> MintQuoteOnchainResponse<Q> {
64 pub fn to_string_id(&self) -> MintQuoteOnchainResponse<String> {
66 MintQuoteOnchainResponse {
67 quote: self.quote.to_string(),
68 request: self.request.clone(),
69 unit: self.unit.clone(),
70 method: self.method.clone(),
71 expiry: self.expiry,
72 pubkey: self.pubkey,
73 amount_paid: self.amount_paid,
74 amount_issued: self.amount_issued,
75 updated_at: self.updated_at,
76 }
77 }
78}
79
80#[cfg(feature = "mint")]
81impl From<MintQuoteOnchainResponse<QuoteId>> for MintQuoteOnchainResponse<String> {
82 fn from(value: MintQuoteOnchainResponse<QuoteId>) -> Self {
83 Self {
84 quote: value.quote.to_string(),
85 request: value.request,
86 unit: value.unit,
87 method: value.method,
88 expiry: value.expiry,
89 pubkey: value.pubkey,
90 amount_paid: value.amount_paid,
91 amount_issued: value.amount_issued,
92 updated_at: value.updated_at,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct MeltQuoteOnchainRequest {
102 pub request: String,
104 pub unit: CurrencyUnit,
106 pub amount: Amount,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(bound = "Q: Serialize + DeserializeOwned")]
116pub struct MeltOnchainRequest<Q> {
117 pub quote: Q,
119 pub fee_index: u32,
121 pub inputs: Proofs,
123 pub outputs: Option<Vec<BlindedMessage>>,
125}
126
127impl<Q> From<MeltOnchainRequest<Q>> for MeltRequest<Q>
128where
129 Q: Serialize + DeserializeOwned,
130{
131 fn from(request: MeltOnchainRequest<Q>) -> Self {
132 MeltRequest::new(request.quote, request.inputs, request.outputs)
133 .fee_index(request.fee_index)
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
150pub struct MeltQuoteOnchainFeeOption {
151 pub fee_index: u32,
153 pub fee_reserve: Amount,
155 pub estimated_blocks: u32,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(bound = "Q: Serialize + DeserializeOwned")]
169pub struct MeltQuoteOnchainResponse<Q> {
170 pub quote: Q,
172 pub amount: Amount,
174 pub unit: CurrencyUnit,
176 #[serde(default = "default_onchain_method")]
178 pub method: PaymentMethod,
179 pub state: MeltQuoteState,
181 pub expiry: u64,
183 pub request: String,
185 pub fee_options: Vec<MeltQuoteOnchainFeeOption>,
192 pub selected_fee_index: Option<u32>,
194 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
196 pub outpoint: Option<String>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub change: Option<Vec<BlindSignature>>,
200}
201
202impl<Q: ToString> MeltQuoteOnchainResponse<Q> {
203 pub fn to_string_id(&self) -> MeltQuoteOnchainResponse<String> {
205 MeltQuoteOnchainResponse {
206 quote: self.quote.to_string(),
207 amount: self.amount,
208 unit: self.unit.clone(),
209 method: self.method.clone(),
210 state: self.state,
211 expiry: self.expiry,
212 request: self.request.clone(),
213 fee_options: self.fee_options.clone(),
214 selected_fee_index: self.selected_fee_index,
215 outpoint: self.outpoint.clone(),
216 change: self.change.clone(),
217 }
218 }
219}
220
221#[cfg(feature = "mint")]
222impl From<MeltQuoteOnchainResponse<QuoteId>> for MeltQuoteOnchainResponse<String> {
223 fn from(value: MeltQuoteOnchainResponse<QuoteId>) -> Self {
224 Self {
225 quote: value.quote.to_string(),
226 amount: value.amount,
227 unit: value.unit,
228 method: value.method,
229 state: value.state,
230 expiry: value.expiry,
231 request: value.request,
232 fee_options: value.fee_options,
233 selected_fee_index: value.selected_fee_index,
234 outpoint: value.outpoint,
235 change: value.change,
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_mint_quote_onchain_request_serialization() {
246 let request = MintQuoteOnchainRequest {
247 unit: CurrencyUnit::Sat,
248 pubkey: PublicKey::from_hex(
249 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
250 )
251 .unwrap(),
252 };
253
254 let serialized = serde_json::to_string(&request).unwrap();
255 let deserialized: MintQuoteOnchainRequest = serde_json::from_str(&serialized).unwrap();
256
257 assert_eq!(request.unit, deserialized.unit);
258 assert_eq!(request.pubkey, deserialized.pubkey);
259 }
260
261 #[test]
262 fn test_melt_quote_onchain_request_serialization() {
263 let request = MeltQuoteOnchainRequest {
264 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
265 unit: CurrencyUnit::Sat,
266 amount: Amount::from(1000),
267 };
268
269 let serialized = serde_json::to_string(&request).unwrap();
270 let deserialized: MeltQuoteOnchainRequest = serde_json::from_str(&serialized).unwrap();
271
272 assert_eq!(request.request, deserialized.request);
273 assert_eq!(request.unit, deserialized.unit);
274 assert_eq!(request.amount, deserialized.amount);
275 }
276
277 #[test]
278 fn test_melt_quote_onchain_response_serialization() {
279 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
280 quote: "TRmjduhIsPxd...".to_string(),
281 amount: Amount::from(100000),
282 unit: CurrencyUnit::Sat,
283 method: PaymentMethod::Known(crate::nuts::nut00::KnownMethod::Onchain),
284 state: MeltQuoteState::Pending,
285 expiry: 1701704757,
286 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
287 fee_options: vec![MeltQuoteOnchainFeeOption {
288 fee_index: 0,
289 fee_reserve: Amount::from(5000),
290 estimated_blocks: 1,
291 }],
292 selected_fee_index: Some(0),
293 outpoint: Some(
294 "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2".to_string(),
295 ),
296 change: None,
297 };
298
299 let serialized = serde_json::to_string(&response).unwrap();
300 assert!(serialized.contains("\"fee_reserve\""));
301 assert!(serialized.contains("\"fee_index\""));
302 assert!(!serialized.contains("\"fee\":"));
303
304 let deserialized: MeltQuoteOnchainResponse<String> =
305 serde_json::from_str(&serialized).unwrap();
306
307 assert_eq!(response.quote, deserialized.quote);
308 assert_eq!(response.request, deserialized.request);
309 assert_eq!(response.amount, deserialized.amount);
310 assert_eq!(response.method, deserialized.method);
311 assert_eq!(response.fee_options, deserialized.fee_options);
312 assert_eq!(response.selected_fee_index, deserialized.selected_fee_index);
313 assert_eq!(response.state, deserialized.state);
314 assert_eq!(response.outpoint, deserialized.outpoint);
315 assert_eq!(response.change, deserialized.change);
316 }
317
318 #[test]
319 fn test_melt_quote_onchain_response_defaults_method() {
320 let value = serde_json::json!({
321 "quote": "TRmjduhIsPxd...",
322 "amount": 100000,
323 "unit": "sat",
324 "state": "PENDING",
325 "expiry": 1701704757,
326 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
327 "fee_options": [
328 {
329 "fee_index": 0,
330 "fee_reserve": 5000,
331 "estimated_blocks": 1
332 }
333 ],
334 "selected_fee_index": 0
335 });
336
337 let decoded: MeltQuoteOnchainResponse<String> =
338 serde_json::from_value(value).expect("deserialize response");
339 assert_eq!(decoded.method, PaymentMethod::Known(KnownMethod::Onchain));
340 }
341
342 #[test]
343 fn test_melt_quote_onchain_response_serializes_null_outpoint() {
344 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
345 quote: "TRmjduhIsPxd...".to_string(),
346 amount: Amount::from(100000),
347 unit: CurrencyUnit::Sat,
348 method: PaymentMethod::Known(crate::nuts::nut00::KnownMethod::Onchain),
349 state: MeltQuoteState::Pending,
350 expiry: 1701704757,
351 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
352 fee_options: vec![MeltQuoteOnchainFeeOption {
353 fee_index: 0,
354 fee_reserve: Amount::from(5000),
355 estimated_blocks: 1,
356 }],
357 selected_fee_index: None,
358 outpoint: None,
359 change: None,
360 };
361
362 let serialized = serde_json::to_string(&response).unwrap();
363 assert!(serialized.contains("\"outpoint\":null"));
364
365 let deserialized: MeltQuoteOnchainResponse<String> =
366 serde_json::from_str(&serialized).unwrap();
367 assert_eq!(deserialized.outpoint, None);
368 }
369
370 #[test]
371 fn test_mint_quote_onchain_response_tolerates_unknown_fields() {
372 let encoded = r#"{
375 "quote": "DSGLX9kevM...",
376 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
377 "unit": "sat",
378 "expiry": 1701704757,
379 "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
380 "amount_paid": 100000,
381 "amount_issued": 0,
382 "payjoin": {"endpoint": "https://payjoin.example/pj"}
383 }"#;
384
385 let deserialized: MintQuoteOnchainResponse<String> = serde_json::from_str(encoded).unwrap();
386 assert_eq!(deserialized.quote, "DSGLX9kevM...");
387 assert_eq!(deserialized.amount_paid, Amount::from(100000));
388 }
389
390 #[test]
391 fn test_melt_quote_onchain_response_tolerates_unknown_fields() {
392 let encoded = r#"{
393 "quote": "TRmjduhIsPxd...",
394 "amount": 100000,
395 "unit": "sat",
396 "state": "PENDING",
397 "expiry": 1701704757,
398 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
399 "fee_options": [{"fee_index": 0, "fee_reserve": 5000, "estimated_blocks": 1}],
400 "selected_fee_index": null,
401 "outpoint": null,
402 "payjoin": {"endpoint": "https://payjoin.example/pj"}
403 }"#;
404
405 let deserialized: MeltQuoteOnchainResponse<String> = serde_json::from_str(encoded).unwrap();
406 assert_eq!(deserialized.quote, "TRmjduhIsPxd...");
407 assert_eq!(deserialized.state, MeltQuoteState::Pending);
408 }
409
410 #[test]
411 fn test_mint_quote_onchain_response_to_string_id() {
412 use crate::nuts::nut00::CurrencyUnit;
413 use crate::nuts::nut01::PublicKey;
414 use crate::Amount;
415
416 let response: MintQuoteOnchainResponse<String> = MintQuoteOnchainResponse {
417 quote: "DSGLX9kevM...".to_string(),
418 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
419 unit: CurrencyUnit::Sat,
420 method: PaymentMethod::Known(crate::nuts::nut00::KnownMethod::Onchain),
421 expiry: Some(1701704757),
422 pubkey: PublicKey::from_hex(
423 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
424 )
425 .unwrap(),
426 amount_paid: Amount::from(100000),
427 amount_issued: Amount::from(0),
428 updated_at: 0,
429 };
430
431 let string_id_response = response.to_string_id();
432 assert_eq!(string_id_response.quote, "DSGLX9kevM...");
433 }
434
435 #[test]
436 fn test_mint_quote_onchain_response_defaults_method() {
437 let value = serde_json::json!({
438 "quote": "DSGLX9kevM...",
439 "request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
440 "unit": "sat",
441 "expiry": 1701704757,
442 "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
443 "amount_paid": 100000,
444 "amount_issued": 0
445 });
446
447 let decoded: MintQuoteOnchainResponse<String> =
448 serde_json::from_value(value).expect("deserialize response");
449 assert_eq!(decoded.method, PaymentMethod::Known(KnownMethod::Onchain));
450 }
451
452 #[test]
453 fn test_melt_quote_onchain_response_to_string_id() {
454 use crate::nuts::nut00::CurrencyUnit;
455 use crate::Amount;
456
457 let response: MeltQuoteOnchainResponse<String> = MeltQuoteOnchainResponse {
458 quote: "TRmjduhIsPxd...".to_string(),
459 amount: Amount::from(100000),
460 unit: CurrencyUnit::Sat,
461 method: PaymentMethod::Known(crate::nuts::nut00::KnownMethod::Onchain),
462 state: MeltQuoteState::Pending,
463 expiry: 1701704757,
464 request: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
465 fee_options: vec![MeltQuoteOnchainFeeOption {
466 fee_index: 0,
467 fee_reserve: Amount::from(5000),
468 estimated_blocks: 1,
469 }],
470 selected_fee_index: Some(0),
471 outpoint: Some(
472 "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2".to_string(),
473 ),
474 change: None,
475 };
476
477 let string_id_response = response.to_string_id();
478 assert_eq!(string_id_response.quote, "TRmjduhIsPxd...");
479 }
480}