1use std::fmt;
6use std::str::FromStr;
7
8use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
9use serde::ser::{SerializeStruct, Serializer};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
14use super::ProofsMethods;
15use crate::nut00::KnownMethod;
16#[cfg(feature = "mint")]
17use crate::quote_id::QuoteId;
18use crate::Amount;
19
20#[derive(Debug, Error)]
22pub enum Error {
23 #[error("Unknown quote state")]
25 UnknownState,
26 #[error("Amount Overflow")]
28 AmountOverflow,
29 #[error("Unsupported unit")]
31 UnsupportedUnit,
32 #[error("Invalid quote id")]
34 InvalidQuote,
35}
36
37#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
39#[serde(rename_all = "UPPERCASE")]
40pub enum QuoteState {
41 #[default]
43 Unpaid,
44 Paid,
46 Pending,
48 Unknown,
50 Failed,
52}
53
54impl fmt::Display for QuoteState {
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 match self {
57 Self::Unpaid => write!(f, "UNPAID"),
58 Self::Paid => write!(f, "PAID"),
59 Self::Pending => write!(f, "PENDING"),
60 Self::Unknown => write!(f, "UNKNOWN"),
61 Self::Failed => write!(f, "FAILED"),
62 }
63 }
64}
65
66impl FromStr for QuoteState {
67 type Err = Error;
68
69 fn from_str(state: &str) -> Result<Self, Self::Err> {
70 match state {
71 "PENDING" => Ok(Self::Pending),
72 "PAID" => Ok(Self::Paid),
73 "UNPAID" => Ok(Self::Unpaid),
74 "UNKNOWN" => Ok(Self::Unknown),
75 "FAILED" => Ok(Self::Failed),
76 _ => Err(Error::UnknownState),
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(bound = "Q: Serialize + DeserializeOwned")]
84pub struct MeltRequest<Q> {
85 quote: Q,
87 inputs: Proofs,
89 outputs: Option<Vec<BlindedMessage>>,
92 #[serde(default)]
94 prefer_async: bool,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 fee_index: Option<u32>,
98}
99
100#[cfg(feature = "mint")]
101impl TryFrom<MeltRequest<String>> for MeltRequest<QuoteId> {
102 type Error = Error;
103
104 fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
105 Ok(Self {
106 quote: QuoteId::from_str(&value.quote).map_err(|_e| Error::InvalidQuote)?,
107 inputs: value.inputs,
108 outputs: value.outputs,
109 prefer_async: value.prefer_async,
110 fee_index: value.fee_index,
111 })
112 }
113}
114
115impl<Q> MeltRequest<Q> {
117 pub fn quote_id(&self) -> &Q {
119 &self.quote
120 }
121
122 pub fn inputs(&self) -> &Proofs {
124 &self.inputs
125 }
126
127 pub fn inputs_mut(&mut self) -> &mut Proofs {
129 &mut self.inputs
130 }
131
132 pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
134 &self.outputs
135 }
136}
137
138impl<Q> MeltRequest<Q>
139where
140 Q: Serialize + DeserializeOwned,
141{
142 pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
144 Self {
145 quote,
146 inputs: inputs.without_dleqs(),
147 outputs,
148 prefer_async: false,
149 fee_index: None,
150 }
151 }
152
153 pub fn prefer_async(mut self, prefer_async: bool) -> Self {
155 self.prefer_async = prefer_async;
156 self
157 }
158
159 pub fn is_prefer_async(&self) -> bool {
161 self.prefer_async
162 }
163
164 pub fn fee_index(mut self, fee_index: u32) -> Self {
166 self.fee_index = Some(fee_index);
167 self
168 }
169
170 pub fn selected_fee_index(&self) -> Option<u32> {
172 self.fee_index
173 }
174
175 pub fn quote(&self) -> &Q {
177 &self.quote
178 }
179
180 pub fn inputs_amount(&self) -> Result<Amount, Error> {
182 Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
183 .map_err(|_| Error::AmountOverflow)
184 }
185}
186
187impl<Q> super::nut10::SpendingConditionVerification for MeltRequest<Q>
188where
189 Q: std::fmt::Display,
190{
191 fn inputs(&self) -> &Proofs {
192 &self.inputs
193 }
194
195 fn sig_all_msg_to_sign(&self) -> String {
196 let mut msg = String::new();
197
198 for proof in &self.inputs {
201 msg.push_str(&proof.secret.to_string());
202 msg.push_str(&proof.c.to_hex());
203 }
204
205 if let Some(outputs) = &self.outputs {
208 for output in outputs {
209 msg.push_str(&output.amount.to_string());
210 msg.push_str(&output.blinded_secret.to_hex());
211 }
212 }
213
214 msg.push_str(&self.quote.to_string());
217
218 msg
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
224pub struct MeltMethodSettings {
225 pub method: PaymentMethod,
227 pub unit: CurrencyUnit,
229 pub min_amount: Option<Amount>,
231 pub max_amount: Option<Amount>,
233 pub options: Option<MeltMethodOptions>,
235}
236
237impl Serialize for MeltMethodSettings {
238 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
239 where
240 S: Serializer,
241 {
242 let mut num_fields = 3; if self.min_amount.is_some() {
244 num_fields += 1;
245 }
246 if self.max_amount.is_some() {
247 num_fields += 1;
248 }
249
250 let mut amountless_in_top_level = false;
251 if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
252 if *amountless {
253 num_fields += 1;
254 amountless_in_top_level = true;
255 }
256 }
257
258 let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
259
260 state.serialize_field("method", &self.method)?;
261 state.serialize_field("unit", &self.unit)?;
262
263 if let Some(min_amount) = &self.min_amount {
264 state.serialize_field("min_amount", min_amount)?;
265 }
266
267 if let Some(max_amount) = &self.max_amount {
268 state.serialize_field("max_amount", max_amount)?;
269 }
270
271 if amountless_in_top_level {
273 state.serialize_field("amountless", &true)?;
274 }
275
276 state.end()
277 }
278}
279
280struct MeltMethodSettingsVisitor;
281
282impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
283 type Value = MeltMethodSettings;
284
285 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
286 formatter.write_str("a MeltMethodSettings structure")
287 }
288
289 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
290 where
291 M: MapAccess<'de>,
292 {
293 let mut method: Option<PaymentMethod> = None;
294 let mut unit: Option<CurrencyUnit> = None;
295 let mut min_amount: Option<Amount> = None;
296 let mut max_amount: Option<Amount> = None;
297 let mut amountless: Option<bool> = None;
298
299 while let Some(key) = map.next_key::<String>()? {
300 match key.as_str() {
301 "method" => {
302 if method.is_some() {
303 return Err(de::Error::duplicate_field("method"));
304 }
305 method = Some(map.next_value()?);
306 }
307 "unit" => {
308 if unit.is_some() {
309 return Err(de::Error::duplicate_field("unit"));
310 }
311 unit = Some(map.next_value()?);
312 }
313 "min_amount" => {
314 if min_amount.is_some() {
315 return Err(de::Error::duplicate_field("min_amount"));
316 }
317 min_amount = Some(map.next_value()?);
318 }
319 "max_amount" => {
320 if max_amount.is_some() {
321 return Err(de::Error::duplicate_field("max_amount"));
322 }
323 max_amount = Some(map.next_value()?);
324 }
325 "amountless" => {
326 if amountless.is_some() {
327 return Err(de::Error::duplicate_field("amountless"));
328 }
329 amountless = Some(map.next_value()?);
330 }
331 "options" => {
332 let options: Option<MeltMethodOptions> = map.next_value()?;
335
336 if let Some(MeltMethodOptions::Bolt11 {
337 amountless: amountless_from_options,
338 }) = options
339 {
340 if amountless.is_none() {
342 amountless = Some(amountless_from_options);
343 }
344 }
345 }
346 _ => {
347 let _: serde::de::IgnoredAny = map.next_value()?;
349 }
350 }
351 }
352
353 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
354 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
355
356 let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) && amountless.is_some()
358 {
359 amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
360 } else {
361 None
362 };
363
364 Ok(MeltMethodSettings {
365 method,
366 unit,
367 min_amount,
368 max_amount,
369 options,
370 })
371 }
372}
373
374impl<'de> Deserialize<'de> for MeltMethodSettings {
375 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
376 where
377 D: Deserializer<'de>,
378 {
379 deserializer.deserialize_map(MeltMethodSettingsVisitor)
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
385#[serde(untagged)]
386pub enum MeltMethodOptions {
387 Bolt11 {
389 amountless: bool,
391 },
392}
393
394impl Settings {
395 pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
397 Self { methods, disabled }
398 }
399
400 pub fn get_settings(
402 &self,
403 unit: &CurrencyUnit,
404 method: &PaymentMethod,
405 ) -> Option<MeltMethodSettings> {
406 for method_settings in self.methods.iter() {
407 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
408 return Some(method_settings.clone());
409 }
410 }
411
412 None
413 }
414
415 pub fn remove_settings(
417 &mut self,
418 unit: &CurrencyUnit,
419 method: &PaymentMethod,
420 ) -> Option<MeltMethodSettings> {
421 self.methods
422 .iter()
423 .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
424 .map(|index| self.methods.remove(index))
425 }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
430pub struct Settings {
431 pub methods: Vec<MeltMethodSettings>,
433 pub disabled: bool,
435}
436
437impl Settings {
438 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
440 self.methods.iter().map(|a| &a.method).collect()
441 }
442
443 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
445 self.methods.iter().map(|s| &s.unit).collect()
446 }
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456pub struct MeltQuoteCustomRequest {
457 pub method: String,
459 pub request: String,
461 pub unit: CurrencyUnit,
463 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
468 pub extra: serde_json::Value,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
496pub struct MeltQuoteCustomResponse<Q> {
497 pub quote: Q,
499 pub amount: Amount,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub fee_reserve: Option<Amount>,
504 pub state: QuoteState,
506 pub expiry: u64,
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub payment_preimage: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub change: Option<Vec<BlindSignature>>,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub request: Option<String>,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub unit: Option<CurrencyUnit>,
520 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
525 pub extra: serde_json::Value,
526}
527
528impl<Q: ToString> MeltQuoteCustomResponse<Q> {
529 pub fn to_string_id(&self) -> MeltQuoteCustomResponse<String> {
531 MeltQuoteCustomResponse {
532 quote: self.quote.to_string(),
533 amount: self.amount,
534 fee_reserve: self.fee_reserve,
535 state: self.state,
536 expiry: self.expiry,
537 payment_preimage: self.payment_preimage.clone(),
538 change: self.change.clone(),
539 request: self.request.clone(),
540 unit: self.unit.clone(),
541 extra: self.extra.clone(),
542 }
543 }
544}
545
546#[cfg(feature = "mint")]
547impl From<MeltQuoteCustomResponse<QuoteId>> for MeltQuoteCustomResponse<String> {
548 fn from(value: MeltQuoteCustomResponse<QuoteId>) -> Self {
549 Self {
550 quote: value.quote.to_string(),
551 amount: value.amount,
552 fee_reserve: value.fee_reserve,
553 state: value.state,
554 expiry: value.expiry,
555 payment_preimage: value.payment_preimage,
556 change: value.change,
557 request: value.request,
558 unit: value.unit,
559 extra: value.extra,
560 }
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use serde_json::{from_str, json, to_string};
567
568 use super::*;
569 use crate::nut00::KnownMethod;
570
571 #[test]
572 fn test_melt_method_settings_top_level_amountless() {
573 let json_str = r#"{
575 "method": "bolt11",
576 "unit": "sat",
577 "min_amount": 0,
578 "max_amount": 10000,
579 "amountless": true
580 }"#;
581
582 let settings: MeltMethodSettings = from_str(json_str).unwrap();
584
585 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
587 assert_eq!(settings.unit, CurrencyUnit::Sat);
588 assert_eq!(settings.min_amount, Some(Amount::from(0)));
589 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
590
591 match settings.options {
592 Some(MeltMethodOptions::Bolt11 { amountless }) => {
593 assert!(amountless);
594 }
595 _ => panic!("Expected Bolt11 options with amountless = true"),
596 }
597
598 let serialized = to_string(&settings).unwrap();
600 let parsed: serde_json::Value = from_str(&serialized).unwrap();
601
602 assert_eq!(parsed["amountless"], json!(true));
604 }
605
606 #[test]
607 fn test_both_amountless_locations() {
608 let json_str = r#"{
610 "method": "bolt11",
611 "unit": "sat",
612 "min_amount": 0,
613 "max_amount": 10000,
614 "amountless": true,
615 "options": {
616 "amountless": false
617 }
618 }"#;
619
620 let settings: MeltMethodSettings = from_str(json_str).unwrap();
622
623 match settings.options {
624 Some(MeltMethodOptions::Bolt11 { amountless }) => {
625 assert!(amountless, "Top-level amountless should take precedence");
626 }
627 _ => panic!("Expected Bolt11 options with amountless = true"),
628 }
629 }
630
631 #[test]
632 fn test_melt_quote_custom_response_fee_reserve_optional() {
633 let json_str = r#"{
634 "quote": "abc123",
635 "state": "UNPAID",
636 "amount": 1000,
637 "expiry": 1234567890,
638 "custom_field": "value"
639 }"#;
640
641 let response: MeltQuoteCustomResponse<String> = from_str(json_str).unwrap();
642
643 assert_eq!(response.fee_reserve, None);
644 assert_eq!(response.extra["custom_field"], json!("value"));
645
646 let serialized = to_string(&response).unwrap();
647 let parsed: serde_json::Value = from_str(&serialized).unwrap();
648
649 assert!(parsed.get("fee_reserve").is_none());
650 }
651
652 #[test]
653 fn test_melt_quote_custom_response_serializes_fee_reserve_when_present() {
654 let response = MeltQuoteCustomResponse {
655 quote: "abc123".to_string(),
656 amount: Amount::from(1000),
657 fee_reserve: Some(Amount::from(10)),
658 state: QuoteState::Unpaid,
659 expiry: 1234567890,
660 payment_preimage: None,
661 change: None,
662 request: None,
663 unit: None,
664 extra: serde_json::Value::Null,
665 };
666
667 let serialized = to_string(&response).unwrap();
668 let parsed: serde_json::Value = from_str(&serialized).unwrap();
669
670 assert_eq!(parsed["fee_reserve"], json!(10));
671 }
672}