1use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6use crate::nuts::nut00::KnownMethod;
7use crate::nuts::nut05::{MeltQuoteCustomRequest, MeltQuoteCustomResponse};
8use crate::nuts::nut23::{MeltQuoteBolt11Request, MeltQuoteBolt11Response};
9use crate::nuts::nut25::{MeltQuoteBolt12Request, MeltQuoteBolt12Response};
10use crate::nuts::nut30::{MeltQuoteOnchainRequest, MeltQuoteOnchainResponse};
11use crate::{Amount, CurrencyUnit, MeltQuoteState, PaymentMethod};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum MeltQuoteRequest {
19 Bolt11(MeltQuoteBolt11Request),
21 Bolt12(MeltQuoteBolt12Request),
23 Onchain(MeltQuoteOnchainRequest),
25 Custom(MeltQuoteCustomRequest),
27}
28
29impl From<MeltQuoteBolt11Request> for MeltQuoteRequest {
30 fn from(request: MeltQuoteBolt11Request) -> Self {
31 MeltQuoteRequest::Bolt11(request)
32 }
33}
34
35impl From<MeltQuoteBolt12Request> for MeltQuoteRequest {
36 fn from(request: MeltQuoteBolt12Request) -> Self {
37 MeltQuoteRequest::Bolt12(request)
38 }
39}
40
41impl From<MeltQuoteOnchainRequest> for MeltQuoteRequest {
42 fn from(request: MeltQuoteOnchainRequest) -> Self {
43 MeltQuoteRequest::Onchain(request)
44 }
45}
46
47impl From<MeltQuoteCustomRequest> for MeltQuoteRequest {
48 fn from(request: MeltQuoteCustomRequest) -> Self {
49 MeltQuoteRequest::Custom(request)
50 }
51}
52
53impl MeltQuoteRequest {
54 pub fn method(&self) -> PaymentMethod {
56 match self {
57 Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
58 Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
59 Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
60 Self::Custom(request) => PaymentMethod::from(request.method.as_str()),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(bound = "Q: Serialize + DeserializeOwned")]
68pub enum MeltQuoteResponse<Q> {
69 Bolt11(MeltQuoteBolt11Response<Q>),
71 Bolt12(MeltQuoteBolt12Response<Q>),
73 Onchain(MeltQuoteOnchainResponse<Q>),
75 Custom((PaymentMethod, MeltQuoteCustomResponse<Q>)),
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(bound = "Q: Serialize + DeserializeOwned")]
82pub enum MeltQuoteCreateResponse<Q> {
83 Bolt11(MeltQuoteBolt11Response<Q>),
85 Bolt12(MeltQuoteBolt12Response<Q>),
87 Onchain(MeltQuoteOnchainResponse<Q>),
89 Custom((PaymentMethod, MeltQuoteCustomResponse<Q>)),
91}
92
93impl<Q> MeltQuoteResponse<Q> {
94 pub fn method(&self) -> PaymentMethod {
96 match self {
97 Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
98 Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
99 Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
100 Self::Custom((method, _)) => method.clone(),
101 }
102 }
103}
104
105impl<Q: ToString> MeltQuoteResponse<Q> {
106 pub fn to_string_id(self) -> MeltQuoteResponse<String> {
108 match self {
109 Self::Bolt11(r) => MeltQuoteResponse::Bolt11(r.to_string_id()),
110 Self::Bolt12(r) => MeltQuoteResponse::Bolt12(r.to_string_id()),
111 Self::Onchain(r) => MeltQuoteResponse::Onchain(r.to_string_id()),
112 Self::Custom((method, r)) => MeltQuoteResponse::Custom((method, r.to_string_id())),
113 }
114 }
115
116 pub fn quote(&self) -> &Q {
118 match self {
119 Self::Bolt11(r) => &r.quote,
120 Self::Bolt12(r) => &r.quote,
121 Self::Onchain(r) => &r.quote,
122 Self::Custom((_, r)) => &r.quote,
123 }
124 }
125
126 pub fn amount(&self) -> Amount {
128 match self {
129 Self::Bolt11(r) => r.amount,
130 Self::Bolt12(r) => r.amount,
131 Self::Onchain(r) => r.amount,
132 Self::Custom((_, r)) => r.amount,
133 }
134 }
135
136 pub fn fee_reserve(&self) -> Amount {
138 match self {
139 Self::Bolt11(r) => r.fee_reserve,
140 Self::Bolt12(r) => r.fee_reserve,
141 Self::Onchain(r) => r
142 .selected_fee_index
143 .and_then(|selected| {
144 r.fee_options
145 .iter()
146 .find(|option| option.fee_index == selected)
147 })
148 .or_else(|| r.fee_options.first())
149 .map(|option| option.fee_reserve)
150 .unwrap_or(Amount::ZERO),
151 Self::Custom((_, r)) => r.fee_reserve.unwrap_or_default(),
152 }
153 }
154
155 pub fn state(&self) -> MeltQuoteState {
157 match self {
158 Self::Bolt11(r) => r.state,
159 Self::Bolt12(r) => r.state,
160 Self::Onchain(r) => r.state,
161 Self::Custom((_, r)) => r.state,
162 }
163 }
164
165 pub fn expiry(&self) -> u64 {
167 match self {
168 Self::Bolt11(r) => r.expiry,
169 Self::Bolt12(r) => r.expiry,
170 Self::Onchain(r) => r.expiry,
171 Self::Custom((_, r)) => r.expiry,
172 }
173 }
174
175 pub fn payment_proof(&self) -> Option<&str> {
185 match self {
186 Self::Bolt11(r) => r.payment_preimage.as_deref(),
187 Self::Bolt12(r) => r.payment_preimage.as_deref(),
188 Self::Onchain(r) => r.outpoint.as_deref(),
189 Self::Custom((_, r)) => r.payment_preimage.as_deref(),
190 }
191 }
192
193 pub fn change(&self) -> Option<&Vec<crate::BlindSignature>> {
195 match self {
196 Self::Bolt11(r) => r.change.as_ref(),
197 Self::Bolt12(r) => r.change.as_ref(),
198 Self::Onchain(r) => r.change.as_ref(),
199 Self::Custom((_, r)) => r.change.as_ref(),
200 }
201 }
202
203 pub fn request(&self) -> Option<&str> {
205 match self {
206 Self::Bolt11(r) => r.request.as_deref(),
207 Self::Bolt12(r) => r.request.as_deref(),
208 Self::Onchain(r) => Some(r.request.as_str()),
209 Self::Custom((_, r)) => r.request.as_deref(),
210 }
211 }
212
213 pub fn unit(&self) -> Option<CurrencyUnit> {
215 match self {
216 Self::Bolt11(r) => r.unit.clone(),
217 Self::Bolt12(r) => r.unit.clone(),
218 Self::Onchain(r) => Some(r.unit.clone()),
219 Self::Custom((_, r)) => r.unit.clone(),
220 }
221 }
222}
223
224#[cfg(feature = "mint")]
225impl From<MeltQuoteResponse<crate::QuoteId>> for MeltQuoteResponse<String> {
226 fn from(value: MeltQuoteResponse<crate::QuoteId>) -> Self {
227 value.to_string_id()
228 }
229}
230
231impl<Q: ToString> MeltQuoteCreateResponse<Q> {
232 pub fn to_string_id(self) -> MeltQuoteCreateResponse<String> {
234 match self {
235 Self::Bolt11(r) => MeltQuoteCreateResponse::Bolt11(r.to_string_id()),
236 Self::Bolt12(r) => MeltQuoteCreateResponse::Bolt12(r.to_string_id()),
237 Self::Onchain(r) => MeltQuoteCreateResponse::Onchain(r.to_string_id()),
238 Self::Custom((method, r)) => {
239 MeltQuoteCreateResponse::Custom((method, r.to_string_id()))
240 }
241 }
242 }
243
244 pub fn method(&self) -> PaymentMethod {
246 match self {
247 Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
248 Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
249 Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
250 Self::Custom((method, _)) => method.clone(),
251 }
252 }
253
254 pub fn quote(&self) -> Option<&Q> {
256 match self {
257 Self::Bolt11(r) => Some(&r.quote),
258 Self::Bolt12(r) => Some(&r.quote),
259 Self::Onchain(r) => Some(&r.quote),
260 Self::Custom((_, r)) => Some(&r.quote),
261 }
262 }
263}
264
265#[cfg(feature = "mint")]
266impl From<MeltQuoteCreateResponse<crate::QuoteId>> for MeltQuoteCreateResponse<String> {
267 fn from(value: MeltQuoteCreateResponse<crate::QuoteId>) -> Self {
268 value.to_string_id()
269 }
270}
271
272#[cfg(feature = "mint")]
273impl<Q> From<crate::mint::MeltQuote> for MeltQuoteResponse<Q>
274where
275 Q: From<crate::QuoteId>,
276{
277 fn from(value: crate::mint::MeltQuote) -> Self {
278 match value.payment_method {
279 PaymentMethod::Known(KnownMethod::Bolt11) => {
280 Self::Bolt11(crate::nuts::nut23::MeltQuoteBolt11Response {
281 quote: value.id.clone().into(),
282 amount: value.amount().into(),
283 fee_reserve: value.fee_reserve().into(),
284 state: value.state,
285 expiry: value.expiry,
286 payment_preimage: value.payment_proof.clone(),
287 change: None,
288 request: Some(value.request.to_string()),
289 unit: Some(value.unit.clone()),
290 })
291 }
292 PaymentMethod::Known(KnownMethod::Bolt12) => {
293 Self::Bolt12(crate::nuts::nut25::MeltQuoteBolt12Response {
294 quote: value.id.clone().into(),
295 amount: value.amount().into(),
296 fee_reserve: value.fee_reserve().into(),
297 state: value.state,
298 expiry: value.expiry,
299 payment_preimage: value.payment_proof.clone(),
300 change: None,
301 request: Some(value.request.to_string()),
302 unit: Some(value.unit.clone()),
303 })
304 }
305 PaymentMethod::Known(KnownMethod::Onchain) => {
306 Self::Onchain(crate::nuts::nut30::MeltQuoteOnchainResponse {
307 quote: value.id.clone().into(),
308 amount: value.amount().into(),
309 unit: value.unit.clone(),
310 state: value.state,
311 expiry: value.expiry,
312 request: value.request.to_string(),
313 fee_options: value.fee_options().to_vec(),
314 selected_fee_index: value.selected_fee_index,
315 outpoint: value.payment_proof.clone(),
316 change: None,
317 })
318 }
319 ref method => Self::Custom((
320 method.clone(),
321 crate::nuts::nut05::MeltQuoteCustomResponse {
322 quote: value.id.clone().into(),
323 amount: value.amount().into(),
324 fee_reserve: Some(value.fee_reserve().into()),
325 state: value.state,
326 expiry: value.expiry,
327 payment_preimage: value.payment_proof.clone(),
328 change: None,
329 request: Some(value.request.to_string()),
330 unit: Some(value.unit.clone()),
331 extra: serde_json::Value::Null,
332 },
333 )),
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::nuts::nut05::MeltQuoteCustomResponse;
342 use crate::nuts::nut23::MeltQuoteBolt11Response;
343 use crate::nuts::nut30::{MeltQuoteOnchainFeeOption, MeltQuoteOnchainResponse};
344 use crate::{Amount, CurrencyUnit, MeltQuoteState};
345
346 fn bolt11_response(quote: &str) -> MeltQuoteBolt11Response<String> {
347 MeltQuoteBolt11Response {
348 quote: quote.to_string(),
349 amount: Amount::from(100),
350 fee_reserve: Amount::from(1),
351 state: MeltQuoteState::Unpaid,
352 expiry: 1000,
353 payment_preimage: Some("preimage-11".to_string()),
354 change: None,
355 request: Some("lnbc100".to_string()),
356 unit: Some(CurrencyUnit::Sat),
357 }
358 }
359
360 fn bolt12_response(quote: &str) -> MeltQuoteBolt12Response<String> {
361 MeltQuoteBolt12Response {
362 quote: quote.to_string(),
363 amount: Amount::from(200),
364 fee_reserve: Amount::from(2),
365 state: MeltQuoteState::Pending,
366 expiry: 2000,
367 payment_preimage: Some("preimage-12".to_string()),
368 change: None,
369 request: Some("lno200".to_string()),
370 unit: Some(CurrencyUnit::Sat),
371 }
372 }
373
374 fn onchain_response(quote: &str) -> MeltQuoteOnchainResponse<String> {
375 MeltQuoteOnchainResponse {
376 quote: quote.to_string(),
377 amount: Amount::from(400),
378 unit: CurrencyUnit::Sat,
379 state: MeltQuoteState::Paid,
380 expiry: 4000,
381 request: "bc1qonchainaddress".to_string(),
382 fee_options: vec![MeltQuoteOnchainFeeOption {
383 fee_index: 0,
384 fee_reserve: Amount::from(4),
385 estimated_blocks: 6,
386 }],
387 selected_fee_index: Some(0),
388 outpoint: Some("abcd...ef:0".to_string()),
389 change: None,
390 }
391 }
392
393 fn custom_response(quote: &str) -> MeltQuoteCustomResponse<String> {
394 MeltQuoteCustomResponse {
395 quote: quote.to_string(),
396 amount: Amount::from(300),
397 fee_reserve: Some(Amount::from(3)),
398 state: MeltQuoteState::Paid,
399 expiry: 3000,
400 payment_preimage: Some("outpoint-abc".to_string()),
401 change: None,
402 request: Some("bc1qaddress".to_string()),
403 unit: Some(CurrencyUnit::Sat),
404 extra: serde_json::Value::Null,
405 }
406 }
407
408 #[test]
409 fn melt_quote_response_accessors_bolt11() {
410 let r = MeltQuoteResponse::Bolt11(bolt11_response("q11"));
411 assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Bolt11));
412 assert_eq!(r.quote(), "q11");
413 assert_eq!(r.amount(), Amount::from(100));
414 assert_eq!(r.fee_reserve(), Amount::from(1));
415 assert_eq!(r.state(), MeltQuoteState::Unpaid);
416 assert_eq!(r.expiry(), 1000);
417 assert_eq!(r.payment_proof(), Some("preimage-11"));
418 assert!(r.change().is_none());
419 assert_eq!(r.request(), Some("lnbc100"));
420 assert_eq!(r.unit(), Some(CurrencyUnit::Sat));
421 }
422
423 #[test]
424 fn melt_quote_response_accessors_bolt12() {
425 let r = MeltQuoteResponse::Bolt12(bolt12_response("q12"));
426 assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Bolt12));
427 assert_eq!(r.quote(), "q12");
428 assert_eq!(r.amount(), Amount::from(200));
429 assert_eq!(r.fee_reserve(), Amount::from(2));
430 assert_eq!(r.state(), MeltQuoteState::Pending);
431 assert_eq!(r.expiry(), 2000);
432 assert_eq!(r.payment_proof(), Some("preimage-12"));
433 assert_eq!(r.request(), Some("lno200"));
434 }
435
436 #[test]
437 fn melt_quote_response_accessors_onchain() {
438 let r = MeltQuoteResponse::Onchain(onchain_response("qoc"));
439 assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Onchain));
440 assert_eq!(r.quote(), "qoc");
441 assert_eq!(r.amount(), Amount::from(400));
442 assert_eq!(r.fee_reserve(), Amount::from(4));
443 assert_eq!(r.state(), MeltQuoteState::Paid);
444 assert_eq!(r.expiry(), 4000);
445 assert_eq!(r.payment_proof(), Some("abcd...ef:0"));
447 assert!(r.change().is_none());
448 assert_eq!(r.request(), Some("bc1qonchainaddress"));
450 assert_eq!(r.unit(), Some(CurrencyUnit::Sat));
452 }
453
454 #[test]
455 fn melt_quote_response_accessors_custom() {
456 let method = PaymentMethod::from("paypal");
457 let r = MeltQuoteResponse::Custom((method.clone(), custom_response("qc")));
458 assert_eq!(r.method(), method);
459 assert_eq!(r.quote(), "qc");
460 assert_eq!(r.amount(), Amount::from(300));
461 assert_eq!(r.fee_reserve(), Amount::from(3));
462 assert_eq!(r.state(), MeltQuoteState::Paid);
463 assert_eq!(r.expiry(), 3000);
464 assert_eq!(r.payment_proof(), Some("outpoint-abc"));
465 assert_eq!(r.request(), Some("bc1qaddress"));
466 }
467
468 #[test]
469 fn melt_quote_create_response_accessors() {
470 let r11 = MeltQuoteCreateResponse::Bolt11(bolt11_response("c11"));
471 assert_eq!(r11.method(), PaymentMethod::Known(KnownMethod::Bolt11));
472 assert_eq!(r11.quote().map(String::as_str), Some("c11"));
473
474 let r12 = MeltQuoteCreateResponse::Bolt12(bolt12_response("c12"));
475 assert_eq!(r12.method(), PaymentMethod::Known(KnownMethod::Bolt12));
476 assert_eq!(r12.quote().map(String::as_str), Some("c12"));
477
478 let method = PaymentMethod::from("venmo");
479 let rc = MeltQuoteCreateResponse::Custom((method.clone(), custom_response("cc")));
480 assert_eq!(rc.method(), method);
481 assert_eq!(rc.quote().map(String::as_str), Some("cc"));
482 }
483
484 #[test]
485 fn melt_quote_request_method_dispatch() {
486 use crate::nuts::nut05::MeltQuoteCustomRequest;
487
488 let custom_req = MeltQuoteCustomRequest {
489 method: "cashapp".to_string(),
490 unit: CurrencyUnit::Sat,
491 request: "$tag".to_string(),
492 extra: serde_json::Value::Null,
493 };
494 let req: MeltQuoteRequest = custom_req.into();
495 assert_eq!(req.method(), PaymentMethod::from("cashapp"));
496 }
497}