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")]
40#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
41pub enum QuoteState {
42 #[default]
44 Unpaid,
45 Paid,
47 Pending,
49 Unknown,
51 Failed,
53}
54
55impl fmt::Display for QuoteState {
56 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57 match self {
58 Self::Unpaid => write!(f, "UNPAID"),
59 Self::Paid => write!(f, "PAID"),
60 Self::Pending => write!(f, "PENDING"),
61 Self::Unknown => write!(f, "UNKNOWN"),
62 Self::Failed => write!(f, "FAILED"),
63 }
64 }
65}
66
67impl FromStr for QuoteState {
68 type Err = Error;
69
70 fn from_str(state: &str) -> Result<Self, Self::Err> {
71 match state {
72 "PENDING" => Ok(Self::Pending),
73 "PAID" => Ok(Self::Paid),
74 "UNPAID" => Ok(Self::Unpaid),
75 "UNKNOWN" => Ok(Self::Unknown),
76 "FAILED" => Ok(Self::Failed),
77 _ => Err(Error::UnknownState),
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
85#[serde(bound = "Q: Serialize + DeserializeOwned")]
86pub struct MeltRequest<Q> {
87 quote: Q,
89 #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
91 inputs: Proofs,
92 outputs: Option<Vec<BlindedMessage>>,
95 #[serde(default)]
97 #[cfg_attr(feature = "swagger", schema(value_type = bool))]
98 prefer_async: bool,
99}
100
101#[cfg(feature = "mint")]
102impl TryFrom<MeltRequest<String>> for MeltRequest<QuoteId> {
103 type Error = Error;
104
105 fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
106 Ok(Self {
107 quote: QuoteId::from_str(&value.quote).map_err(|_e| Error::InvalidQuote)?,
108 inputs: value.inputs,
109 outputs: value.outputs,
110 prefer_async: value.prefer_async,
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 }
150 }
151
152 pub fn prefer_async(mut self, prefer_async: bool) -> Self {
154 self.prefer_async = prefer_async;
155 self
156 }
157
158 pub fn is_prefer_async(&self) -> bool {
160 self.prefer_async
161 }
162
163 pub fn quote(&self) -> &Q {
165 &self.quote
166 }
167
168 pub fn inputs_amount(&self) -> Result<Amount, Error> {
170 Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
171 .map_err(|_| Error::AmountOverflow)
172 }
173}
174
175impl<Q> super::nut10::SpendingConditionVerification for MeltRequest<Q>
176where
177 Q: std::fmt::Display,
178{
179 fn inputs(&self) -> &Proofs {
180 &self.inputs
181 }
182
183 fn sig_all_msg_to_sign(&self) -> String {
184 let mut msg = String::new();
185
186 for proof in &self.inputs {
189 msg.push_str(&proof.secret.to_string());
190 msg.push_str(&proof.c.to_hex());
191 }
192
193 if let Some(outputs) = &self.outputs {
196 for output in outputs {
197 msg.push_str(&output.amount.to_string());
198 msg.push_str(&output.blinded_secret.to_hex());
199 }
200 }
201
202 msg.push_str(&self.quote.to_string());
205
206 msg
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
212#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
213pub struct MeltMethodSettings {
214 pub method: PaymentMethod,
216 pub unit: CurrencyUnit,
218 pub min_amount: Option<Amount>,
220 pub max_amount: Option<Amount>,
222 pub options: Option<MeltMethodOptions>,
224}
225
226impl Serialize for MeltMethodSettings {
227 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
228 where
229 S: Serializer,
230 {
231 let mut num_fields = 3; if self.min_amount.is_some() {
233 num_fields += 1;
234 }
235 if self.max_amount.is_some() {
236 num_fields += 1;
237 }
238
239 let mut amountless_in_top_level = false;
240 if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
241 if *amountless {
242 num_fields += 1;
243 amountless_in_top_level = true;
244 }
245 }
246
247 let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
248
249 state.serialize_field("method", &self.method)?;
250 state.serialize_field("unit", &self.unit)?;
251
252 if let Some(min_amount) = &self.min_amount {
253 state.serialize_field("min_amount", min_amount)?;
254 }
255
256 if let Some(max_amount) = &self.max_amount {
257 state.serialize_field("max_amount", max_amount)?;
258 }
259
260 if amountless_in_top_level {
262 state.serialize_field("amountless", &true)?;
263 }
264
265 state.end()
266 }
267}
268
269struct MeltMethodSettingsVisitor;
270
271impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
272 type Value = MeltMethodSettings;
273
274 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
275 formatter.write_str("a MeltMethodSettings structure")
276 }
277
278 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
279 where
280 M: MapAccess<'de>,
281 {
282 let mut method: Option<PaymentMethod> = None;
283 let mut unit: Option<CurrencyUnit> = None;
284 let mut min_amount: Option<Amount> = None;
285 let mut max_amount: Option<Amount> = None;
286 let mut amountless: Option<bool> = None;
287
288 while let Some(key) = map.next_key::<String>()? {
289 match key.as_str() {
290 "method" => {
291 if method.is_some() {
292 return Err(de::Error::duplicate_field("method"));
293 }
294 method = Some(map.next_value()?);
295 }
296 "unit" => {
297 if unit.is_some() {
298 return Err(de::Error::duplicate_field("unit"));
299 }
300 unit = Some(map.next_value()?);
301 }
302 "min_amount" => {
303 if min_amount.is_some() {
304 return Err(de::Error::duplicate_field("min_amount"));
305 }
306 min_amount = Some(map.next_value()?);
307 }
308 "max_amount" => {
309 if max_amount.is_some() {
310 return Err(de::Error::duplicate_field("max_amount"));
311 }
312 max_amount = Some(map.next_value()?);
313 }
314 "amountless" => {
315 if amountless.is_some() {
316 return Err(de::Error::duplicate_field("amountless"));
317 }
318 amountless = Some(map.next_value()?);
319 }
320 "options" => {
321 let options: Option<MeltMethodOptions> = map.next_value()?;
324
325 if let Some(MeltMethodOptions::Bolt11 {
326 amountless: amountless_from_options,
327 }) = options
328 {
329 if amountless.is_none() {
331 amountless = Some(amountless_from_options);
332 }
333 }
334 }
335 _ => {
336 let _: serde::de::IgnoredAny = map.next_value()?;
338 }
339 }
340 }
341
342 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
343 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
344
345 let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) && amountless.is_some()
347 {
348 amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
349 } else {
350 None
351 };
352
353 Ok(MeltMethodSettings {
354 method,
355 unit,
356 min_amount,
357 max_amount,
358 options,
359 })
360 }
361}
362
363impl<'de> Deserialize<'de> for MeltMethodSettings {
364 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
365 where
366 D: Deserializer<'de>,
367 {
368 deserializer.deserialize_map(MeltMethodSettingsVisitor)
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
375#[serde(untagged)]
376pub enum MeltMethodOptions {
377 Bolt11 {
379 amountless: bool,
381 },
382}
383
384impl Settings {
385 pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
387 Self { methods, disabled }
388 }
389
390 pub fn get_settings(
392 &self,
393 unit: &CurrencyUnit,
394 method: &PaymentMethod,
395 ) -> Option<MeltMethodSettings> {
396 for method_settings in self.methods.iter() {
397 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
398 return Some(method_settings.clone());
399 }
400 }
401
402 None
403 }
404
405 pub fn remove_settings(
407 &mut self,
408 unit: &CurrencyUnit,
409 method: &PaymentMethod,
410 ) -> Option<MeltMethodSettings> {
411 self.methods
412 .iter()
413 .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
414 .map(|index| self.methods.remove(index))
415 }
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
420#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
421pub struct Settings {
422 pub methods: Vec<MeltMethodSettings>,
424 pub disabled: bool,
426}
427
428impl Settings {
429 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
431 self.methods.iter().map(|a| &a.method).collect()
432 }
433
434 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
436 self.methods.iter().map(|s| &s.unit).collect()
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
448pub struct MeltQuoteCustomRequest {
449 pub method: String,
451 pub request: String,
453 pub unit: CurrencyUnit,
455 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
460 #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
461 pub extra: serde_json::Value,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
488#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
489#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
490pub struct MeltQuoteCustomResponse<Q> {
491 pub quote: Q,
493 pub amount: Amount,
495 pub fee_reserve: Amount,
497 pub state: QuoteState,
499 pub expiry: u64,
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub payment_preimage: Option<String>,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub change: Option<Vec<BlindSignature>>,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub request: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub unit: Option<CurrencyUnit>,
513 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
518 #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
519 pub extra: serde_json::Value,
520}
521
522#[cfg(feature = "mint")]
523impl<Q: ToString> MeltQuoteCustomResponse<Q> {
524 pub fn to_string_id(&self) -> MeltQuoteCustomResponse<String> {
526 MeltQuoteCustomResponse {
527 quote: self.quote.to_string(),
528 amount: self.amount,
529 fee_reserve: self.fee_reserve,
530 state: self.state,
531 expiry: self.expiry,
532 payment_preimage: self.payment_preimage.clone(),
533 change: self.change.clone(),
534 request: self.request.clone(),
535 unit: self.unit.clone(),
536 extra: self.extra.clone(),
537 }
538 }
539}
540
541#[cfg(feature = "mint")]
542impl From<MeltQuoteCustomResponse<QuoteId>> for MeltQuoteCustomResponse<String> {
543 fn from(value: MeltQuoteCustomResponse<QuoteId>) -> Self {
544 Self {
545 quote: value.quote.to_string(),
546 amount: value.amount,
547 fee_reserve: value.fee_reserve,
548 state: value.state,
549 expiry: value.expiry,
550 payment_preimage: value.payment_preimage,
551 change: value.change,
552 request: value.request,
553 unit: value.unit,
554 extra: value.extra,
555 }
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use serde_json::{from_str, json, to_string};
562
563 use super::*;
564 use crate::nut00::KnownMethod;
565
566 #[test]
567 fn test_melt_method_settings_top_level_amountless() {
568 let json_str = r#"{
570 "method": "bolt11",
571 "unit": "sat",
572 "min_amount": 0,
573 "max_amount": 10000,
574 "amountless": true
575 }"#;
576
577 let settings: MeltMethodSettings = from_str(json_str).unwrap();
579
580 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
582 assert_eq!(settings.unit, CurrencyUnit::Sat);
583 assert_eq!(settings.min_amount, Some(Amount::from(0)));
584 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
585
586 match settings.options {
587 Some(MeltMethodOptions::Bolt11 { amountless }) => {
588 assert!(amountless);
589 }
590 _ => panic!("Expected Bolt11 options with amountless = true"),
591 }
592
593 let serialized = to_string(&settings).unwrap();
595 let parsed: serde_json::Value = from_str(&serialized).unwrap();
596
597 assert_eq!(parsed["amountless"], json!(true));
599 }
600
601 #[test]
602 fn test_both_amountless_locations() {
603 let json_str = r#"{
605 "method": "bolt11",
606 "unit": "sat",
607 "min_amount": 0,
608 "max_amount": 10000,
609 "amountless": true,
610 "options": {
611 "amountless": false
612 }
613 }"#;
614
615 let settings: MeltMethodSettings = from_str(json_str).unwrap();
617
618 match settings.options {
619 Some(MeltMethodOptions::Bolt11 { amountless }) => {
620 assert!(amountless, "Top-level amountless should take precedence");
621 }
622 _ => panic!("Expected Bolt11 options with amountless = true"),
623 }
624 }
625}