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::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
14use super::ProofsMethods;
15#[cfg(feature = "mint")]
16use crate::quote_id::QuoteId;
17use crate::Amount;
18
19#[derive(Debug, Error)]
21pub enum Error {
22 #[error("Unknown quote state")]
24 UnknownState,
25 #[error("Amount Overflow")]
27 AmountOverflow,
28 #[error("Unsupported unit")]
30 UnsupportedUnit,
31 #[error("Invalid quote id")]
33 InvalidQuote,
34}
35
36#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
38#[serde(rename_all = "UPPERCASE")]
39#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
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#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
84#[serde(bound = "Q: Serialize + DeserializeOwned")]
85pub struct MeltRequest<Q> {
86 quote: Q,
88 #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
90 inputs: Proofs,
91 outputs: Option<Vec<BlindedMessage>>,
94}
95
96#[cfg(feature = "mint")]
97impl TryFrom<MeltRequest<String>> for MeltRequest<QuoteId> {
98 type Error = Error;
99
100 fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
101 Ok(Self {
102 quote: QuoteId::from_str(&value.quote).map_err(|_e| Error::InvalidQuote)?,
103 inputs: value.inputs,
104 outputs: value.outputs,
105 })
106 }
107}
108
109impl<Q> MeltRequest<Q> {
111 pub fn quote_id(&self) -> &Q {
113 &self.quote
114 }
115
116 pub fn inputs(&self) -> &Proofs {
118 &self.inputs
119 }
120
121 pub fn inputs_mut(&mut self) -> &mut Proofs {
123 &mut self.inputs
124 }
125
126 pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
128 &self.outputs
129 }
130}
131
132impl<Q> MeltRequest<Q>
133where
134 Q: Serialize + DeserializeOwned,
135{
136 pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
138 Self {
139 quote,
140 inputs: inputs.without_dleqs(),
141 outputs,
142 }
143 }
144
145 pub fn quote(&self) -> &Q {
147 &self.quote
148 }
149
150 pub fn inputs_amount(&self) -> Result<Amount, Error> {
152 Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
153 .map_err(|_| Error::AmountOverflow)
154 }
155}
156
157impl<Q> super::nut10::SpendingConditionVerification for MeltRequest<Q>
158where
159 Q: std::fmt::Display,
160{
161 fn inputs(&self) -> &Proofs {
162 &self.inputs
163 }
164
165 fn sig_all_msg_to_sign(&self) -> String {
166 let mut msg = String::new();
167
168 for proof in &self.inputs {
171 msg.push_str(&proof.secret.to_string());
172 msg.push_str(&proof.c.to_hex());
173 }
174
175 if let Some(outputs) = &self.outputs {
178 for output in outputs {
179 msg.push_str(&output.amount.to_string());
180 msg.push_str(&output.blinded_secret.to_hex());
181 }
182 }
183
184 msg.push_str(&self.quote.to_string());
187
188 msg
189 }
190}
191
192#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
194#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
195pub struct MeltMethodSettings {
196 pub method: PaymentMethod,
198 pub unit: CurrencyUnit,
200 pub min_amount: Option<Amount>,
202 pub max_amount: Option<Amount>,
204 pub options: Option<MeltMethodOptions>,
206}
207
208impl Serialize for MeltMethodSettings {
209 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210 where
211 S: Serializer,
212 {
213 let mut num_fields = 3; if self.min_amount.is_some() {
215 num_fields += 1;
216 }
217 if self.max_amount.is_some() {
218 num_fields += 1;
219 }
220
221 let mut amountless_in_top_level = false;
222 if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
223 if *amountless {
224 num_fields += 1;
225 amountless_in_top_level = true;
226 }
227 }
228
229 let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
230
231 state.serialize_field("method", &self.method)?;
232 state.serialize_field("unit", &self.unit)?;
233
234 if let Some(min_amount) = &self.min_amount {
235 state.serialize_field("min_amount", min_amount)?;
236 }
237
238 if let Some(max_amount) = &self.max_amount {
239 state.serialize_field("max_amount", max_amount)?;
240 }
241
242 if amountless_in_top_level {
244 state.serialize_field("amountless", &true)?;
245 }
246
247 state.end()
248 }
249}
250
251struct MeltMethodSettingsVisitor;
252
253impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
254 type Value = MeltMethodSettings;
255
256 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
257 formatter.write_str("a MeltMethodSettings structure")
258 }
259
260 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
261 where
262 M: MapAccess<'de>,
263 {
264 let mut method: Option<PaymentMethod> = None;
265 let mut unit: Option<CurrencyUnit> = None;
266 let mut min_amount: Option<Amount> = None;
267 let mut max_amount: Option<Amount> = None;
268 let mut amountless: Option<bool> = None;
269
270 while let Some(key) = map.next_key::<String>()? {
271 match key.as_str() {
272 "method" => {
273 if method.is_some() {
274 return Err(de::Error::duplicate_field("method"));
275 }
276 method = Some(map.next_value()?);
277 }
278 "unit" => {
279 if unit.is_some() {
280 return Err(de::Error::duplicate_field("unit"));
281 }
282 unit = Some(map.next_value()?);
283 }
284 "min_amount" => {
285 if min_amount.is_some() {
286 return Err(de::Error::duplicate_field("min_amount"));
287 }
288 min_amount = Some(map.next_value()?);
289 }
290 "max_amount" => {
291 if max_amount.is_some() {
292 return Err(de::Error::duplicate_field("max_amount"));
293 }
294 max_amount = Some(map.next_value()?);
295 }
296 "amountless" => {
297 if amountless.is_some() {
298 return Err(de::Error::duplicate_field("amountless"));
299 }
300 amountless = Some(map.next_value()?);
301 }
302 "options" => {
303 let options: Option<MeltMethodOptions> = map.next_value()?;
306
307 if let Some(MeltMethodOptions::Bolt11 {
308 amountless: amountless_from_options,
309 }) = options
310 {
311 if amountless.is_none() {
313 amountless = Some(amountless_from_options);
314 }
315 }
316 }
317 _ => {
318 let _: serde::de::IgnoredAny = map.next_value()?;
320 }
321 }
322 }
323
324 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
325 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
326
327 let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
329 amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
330 } else {
331 None
332 };
333
334 Ok(MeltMethodSettings {
335 method,
336 unit,
337 min_amount,
338 max_amount,
339 options,
340 })
341 }
342}
343
344impl<'de> Deserialize<'de> for MeltMethodSettings {
345 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
346 where
347 D: Deserializer<'de>,
348 {
349 deserializer.deserialize_map(MeltMethodSettingsVisitor)
350 }
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
355#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
356#[serde(untagged)]
357pub enum MeltMethodOptions {
358 Bolt11 {
360 amountless: bool,
362 },
363}
364
365impl Settings {
366 pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
368 Self { methods, disabled }
369 }
370
371 pub fn get_settings(
373 &self,
374 unit: &CurrencyUnit,
375 method: &PaymentMethod,
376 ) -> Option<MeltMethodSettings> {
377 for method_settings in self.methods.iter() {
378 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
379 return Some(method_settings.clone());
380 }
381 }
382
383 None
384 }
385
386 pub fn remove_settings(
388 &mut self,
389 unit: &CurrencyUnit,
390 method: &PaymentMethod,
391 ) -> Option<MeltMethodSettings> {
392 self.methods
393 .iter()
394 .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
395 .map(|index| self.methods.remove(index))
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
401#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
402pub struct Settings {
403 pub methods: Vec<MeltMethodSettings>,
405 pub disabled: bool,
407}
408
409impl Settings {
410 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
412 self.methods.iter().map(|a| &a.method).collect()
413 }
414
415 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
417 self.methods.iter().map(|s| &s.unit).collect()
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use serde_json::{from_str, json, to_string};
424
425 use super::*;
426
427 #[test]
428 fn test_melt_method_settings_top_level_amountless() {
429 let json_str = r#"{
431 "method": "bolt11",
432 "unit": "sat",
433 "min_amount": 0,
434 "max_amount": 10000,
435 "amountless": true
436 }"#;
437
438 let settings: MeltMethodSettings = from_str(json_str).unwrap();
440
441 assert_eq!(settings.method, PaymentMethod::Bolt11);
443 assert_eq!(settings.unit, CurrencyUnit::Sat);
444 assert_eq!(settings.min_amount, Some(Amount::from(0)));
445 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
446
447 match settings.options {
448 Some(MeltMethodOptions::Bolt11 { amountless }) => {
449 assert!(amountless);
450 }
451 _ => panic!("Expected Bolt11 options with amountless = true"),
452 }
453
454 let serialized = to_string(&settings).unwrap();
456 let parsed: serde_json::Value = from_str(&serialized).unwrap();
457
458 assert_eq!(parsed["amountless"], json!(true));
460 }
461
462 #[test]
463 fn test_both_amountless_locations() {
464 let json_str = r#"{
466 "method": "bolt11",
467 "unit": "sat",
468 "min_amount": 0,
469 "max_amount": 10000,
470 "amountless": true,
471 "options": {
472 "amountless": false
473 }
474 }"#;
475
476 let settings: MeltMethodSettings = from_str(json_str).unwrap();
478
479 match settings.options {
480 Some(MeltMethodOptions::Bolt11 { amountless }) => {
481 assert!(amountless, "Top-level amountless should take precedence");
482 }
483 _ => panic!("Expected Bolt11 options with amountless = true"),
484 }
485 }
486}