|
@@ -5,18 +5,16 @@
|
|
|
use std::fmt;
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
-use serde::de::DeserializeOwned;
|
|
|
-use serde::{Deserialize, Deserializer, Serialize};
|
|
|
-use serde_json::Value;
|
|
|
+use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
|
|
|
+use serde::ser::{SerializeStruct, Serializer};
|
|
|
+use serde::{Deserialize, Serialize};
|
|
|
use thiserror::Error;
|
|
|
#[cfg(feature = "mint")]
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
-use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
|
|
|
-use super::nut15::Mpp;
|
|
|
+use super::nut00::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
|
|
|
use super::ProofsMethods;
|
|
|
-use crate::nuts::MeltQuoteState;
|
|
|
-use crate::{Amount, Bolt11Invoice};
|
|
|
+use crate::Amount;
|
|
|
|
|
|
/// NUT05 Error
|
|
|
#[derive(Debug, Error)]
|
|
@@ -27,118 +25,11 @@ pub enum Error {
|
|
|
/// Amount overflow
|
|
|
#[error("Amount Overflow")]
|
|
|
AmountOverflow,
|
|
|
- /// Invalid Amount
|
|
|
- #[error("Invalid Request")]
|
|
|
- InvalidAmountRequest,
|
|
|
/// Unsupported unit
|
|
|
#[error("Unsupported unit")]
|
|
|
UnsupportedUnit,
|
|
|
}
|
|
|
|
|
|
-/// Melt quote request [NUT-05]
|
|
|
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
-pub struct MeltQuoteBolt11Request {
|
|
|
- /// Bolt11 invoice to be paid
|
|
|
- #[cfg_attr(feature = "swagger", schema(value_type = String))]
|
|
|
- pub request: Bolt11Invoice,
|
|
|
- /// Unit wallet would like to pay with
|
|
|
- pub unit: CurrencyUnit,
|
|
|
- /// Payment Options
|
|
|
- pub options: Option<MeltOptions>,
|
|
|
-}
|
|
|
-
|
|
|
-/// Melt Options
|
|
|
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-#[serde(untagged)]
|
|
|
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
-pub enum MeltOptions {
|
|
|
- /// Mpp Options
|
|
|
- Mpp {
|
|
|
- /// MPP
|
|
|
- mpp: Mpp,
|
|
|
- },
|
|
|
- /// Amountless options
|
|
|
- Amountless {
|
|
|
- /// Amountless
|
|
|
- amountless: Amountless,
|
|
|
- },
|
|
|
-}
|
|
|
-
|
|
|
-impl MeltOptions {
|
|
|
- /// Create new [`MeltOptions::Mpp`]
|
|
|
- pub fn new_mpp<A>(amount: A) -> Self
|
|
|
- where
|
|
|
- A: Into<Amount>,
|
|
|
- {
|
|
|
- Self::Mpp {
|
|
|
- mpp: Mpp {
|
|
|
- amount: amount.into(),
|
|
|
- },
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// Create new [`MeltOptions::Amountless`]
|
|
|
- pub fn new_amountless<A>(amount_msat: A) -> Self
|
|
|
- where
|
|
|
- A: Into<Amount>,
|
|
|
- {
|
|
|
- Self::Amountless {
|
|
|
- amountless: Amountless {
|
|
|
- amount_msat: amount_msat.into(),
|
|
|
- },
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// Payment amount
|
|
|
- pub fn amount_msat(&self) -> Amount {
|
|
|
- match self {
|
|
|
- Self::Mpp { mpp } => mpp.amount,
|
|
|
- Self::Amountless { amountless } => amountless.amount_msat,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Amountless payment
|
|
|
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
-pub struct Amountless {
|
|
|
- /// Amount to pay in msat
|
|
|
- pub amount_msat: Amount,
|
|
|
-}
|
|
|
-
|
|
|
-impl MeltQuoteBolt11Request {
|
|
|
- /// Amount from [`MeltQuoteBolt11Request`]
|
|
|
- ///
|
|
|
- /// Amount can either be defined in the bolt11 invoice,
|
|
|
- /// in the request for an amountless bolt11 or in MPP option.
|
|
|
- pub fn amount_msat(&self) -> Result<Amount, Error> {
|
|
|
- let MeltQuoteBolt11Request {
|
|
|
- request,
|
|
|
- unit: _,
|
|
|
- options,
|
|
|
- ..
|
|
|
- } = self;
|
|
|
-
|
|
|
- match options {
|
|
|
- None => Ok(request
|
|
|
- .amount_milli_satoshis()
|
|
|
- .ok_or(Error::InvalidAmountRequest)?
|
|
|
- .into()),
|
|
|
- Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
|
|
|
- Some(MeltOptions::Amountless { amountless }) => {
|
|
|
- let amount = amountless.amount_msat;
|
|
|
- if let Some(amount_msat) = request.amount_milli_satoshis() {
|
|
|
- if amount != amount_msat.into() {
|
|
|
- return Err(Error::InvalidAmountRequest);
|
|
|
- }
|
|
|
- }
|
|
|
- Ok(amount)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
/// Possible states of a quote
|
|
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
|
#[serde(rename_all = "UPPERCASE")]
|
|
@@ -184,177 +75,11 @@ impl FromStr for QuoteState {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/// Melt quote response [NUT-05]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
-#[serde(bound = "Q: Serialize")]
|
|
|
-pub struct MeltQuoteBolt11Response<Q> {
|
|
|
- /// Quote Id
|
|
|
- pub quote: Q,
|
|
|
- /// The amount that needs to be provided
|
|
|
- pub amount: Amount,
|
|
|
- /// The fee reserve that is required
|
|
|
- pub fee_reserve: Amount,
|
|
|
- /// Whether the request haas be paid
|
|
|
- // TODO: To be deprecated
|
|
|
- /// Deprecated
|
|
|
- pub paid: Option<bool>,
|
|
|
- /// Quote State
|
|
|
- pub state: MeltQuoteState,
|
|
|
- /// Unix timestamp until the quote is valid
|
|
|
- pub expiry: u64,
|
|
|
- /// Payment preimage
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub payment_preimage: Option<String>,
|
|
|
- /// Change
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub change: Option<Vec<BlindSignature>>,
|
|
|
- /// Payment request to fulfill
|
|
|
- // REVIEW: This is now required in the spec, we should remove the option once all mints update
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub request: Option<String>,
|
|
|
- /// Unit
|
|
|
- // REVIEW: This is now required in the spec, we should remove the option once all mints update
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- pub unit: Option<CurrencyUnit>,
|
|
|
-}
|
|
|
-
|
|
|
-impl<Q: ToString> MeltQuoteBolt11Response<Q> {
|
|
|
- /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
|
|
|
- /// `MeltQuoteBolt11Response` with `String`
|
|
|
- pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
|
|
|
- MeltQuoteBolt11Response {
|
|
|
- quote: self.quote.to_string(),
|
|
|
- amount: self.amount,
|
|
|
- fee_reserve: self.fee_reserve,
|
|
|
- paid: self.paid,
|
|
|
- state: self.state,
|
|
|
- expiry: self.expiry,
|
|
|
- payment_preimage: self.payment_preimage,
|
|
|
- change: self.change,
|
|
|
- request: self.request,
|
|
|
- unit: self.unit,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#[cfg(feature = "mint")]
|
|
|
-impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
|
|
|
- fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
|
|
|
- Self {
|
|
|
- quote: value.quote.to_string(),
|
|
|
- amount: value.amount,
|
|
|
- fee_reserve: value.fee_reserve,
|
|
|
- paid: value.paid,
|
|
|
- state: value.state,
|
|
|
- expiry: value.expiry,
|
|
|
- payment_preimage: value.payment_preimage,
|
|
|
- change: value.change,
|
|
|
- request: value.request,
|
|
|
- unit: value.unit,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// A custom deserializer is needed until all mints
|
|
|
-// update some will return without the required state.
|
|
|
-impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
|
|
|
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
- where
|
|
|
- D: Deserializer<'de>,
|
|
|
- {
|
|
|
- let value = Value::deserialize(deserializer)?;
|
|
|
-
|
|
|
- let quote: Q = serde_json::from_value(
|
|
|
- value
|
|
|
- .get("quote")
|
|
|
- .ok_or(serde::de::Error::missing_field("quote"))?
|
|
|
- .clone(),
|
|
|
- )
|
|
|
- .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
|
|
|
-
|
|
|
- let amount = value
|
|
|
- .get("amount")
|
|
|
- .ok_or(serde::de::Error::missing_field("amount"))?
|
|
|
- .as_u64()
|
|
|
- .ok_or(serde::de::Error::missing_field("amount"))?;
|
|
|
- let amount = Amount::from(amount);
|
|
|
-
|
|
|
- let fee_reserve = value
|
|
|
- .get("fee_reserve")
|
|
|
- .ok_or(serde::de::Error::missing_field("fee_reserve"))?
|
|
|
- .as_u64()
|
|
|
- .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
|
|
|
-
|
|
|
- let fee_reserve = Amount::from(fee_reserve);
|
|
|
-
|
|
|
- let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
|
|
|
-
|
|
|
- let state: Option<String> = value
|
|
|
- .get("state")
|
|
|
- .and_then(|s| serde_json::from_value(s.clone()).ok());
|
|
|
-
|
|
|
- let (state, paid) = match (state, paid) {
|
|
|
- (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
|
|
|
- (Some(state), _) => {
|
|
|
- let state: QuoteState = QuoteState::from_str(&state)
|
|
|
- .map_err(|_| serde::de::Error::custom("Unknown state"))?;
|
|
|
- let paid = state == QuoteState::Paid;
|
|
|
-
|
|
|
- (state, paid)
|
|
|
- }
|
|
|
- (None, Some(paid)) => {
|
|
|
- let state = if paid {
|
|
|
- QuoteState::Paid
|
|
|
- } else {
|
|
|
- QuoteState::Unpaid
|
|
|
- };
|
|
|
- (state, paid)
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- let expiry = value
|
|
|
- .get("expiry")
|
|
|
- .ok_or(serde::de::Error::missing_field("expiry"))?
|
|
|
- .as_u64()
|
|
|
- .ok_or(serde::de::Error::missing_field("expiry"))?;
|
|
|
-
|
|
|
- let payment_preimage: Option<String> = value
|
|
|
- .get("payment_preimage")
|
|
|
- .and_then(|p| serde_json::from_value(p.clone()).ok());
|
|
|
-
|
|
|
- let change: Option<Vec<BlindSignature>> = value
|
|
|
- .get("change")
|
|
|
- .and_then(|b| serde_json::from_value(b.clone()).ok());
|
|
|
-
|
|
|
- let request: Option<String> = value
|
|
|
- .get("request")
|
|
|
- .and_then(|r| serde_json::from_value(r.clone()).ok());
|
|
|
-
|
|
|
- let unit: Option<CurrencyUnit> = value
|
|
|
- .get("unit")
|
|
|
- .and_then(|u| serde_json::from_value(u.clone()).ok());
|
|
|
-
|
|
|
- Ok(Self {
|
|
|
- quote,
|
|
|
- amount,
|
|
|
- fee_reserve,
|
|
|
- paid: Some(paid),
|
|
|
- state,
|
|
|
- expiry,
|
|
|
- payment_preimage,
|
|
|
- change,
|
|
|
- request,
|
|
|
- unit,
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
/// Melt Bolt11 Request [NUT-05]
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
#[serde(bound = "Q: Serialize + DeserializeOwned")]
|
|
|
-pub struct MeltBolt11Request<Q> {
|
|
|
+pub struct MeltRequest<Q> {
|
|
|
/// Quote ID
|
|
|
quote: Q,
|
|
|
/// Proofs
|
|
@@ -366,10 +91,10 @@ pub struct MeltBolt11Request<Q> {
|
|
|
}
|
|
|
|
|
|
#[cfg(feature = "mint")]
|
|
|
-impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
|
|
|
+impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
|
|
|
type Error = uuid::Error;
|
|
|
|
|
|
- fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
|
|
|
+ fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
|
|
|
Ok(Self {
|
|
|
quote: Uuid::from_str(&value.quote)?,
|
|
|
inputs: value.inputs,
|
|
@@ -379,7 +104,7 @@ impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
|
|
|
}
|
|
|
|
|
|
// Basic implementation without trait bounds
|
|
|
-impl<Q> MeltBolt11Request<Q> {
|
|
|
+impl<Q> MeltRequest<Q> {
|
|
|
/// Get inputs (proofs)
|
|
|
pub fn inputs(&self) -> &Proofs {
|
|
|
&self.inputs
|
|
@@ -391,8 +116,8 @@ impl<Q> MeltBolt11Request<Q> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
|
|
|
- /// Create new [`MeltBolt11Request`]
|
|
|
+impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
|
|
|
+ /// Create new [`MeltRequest`]
|
|
|
pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
|
|
|
Self {
|
|
|
quote,
|
|
@@ -414,7 +139,7 @@ impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
|
|
|
}
|
|
|
|
|
|
/// Melt Method Settings
|
|
|
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
|
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
pub struct MeltMethodSettings {
|
|
|
/// Payment Method e.g. bolt11
|
|
@@ -422,14 +147,168 @@ pub struct MeltMethodSettings {
|
|
|
/// Currency Unit e.g. sat
|
|
|
pub unit: CurrencyUnit,
|
|
|
/// Min Amount
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
pub min_amount: Option<Amount>,
|
|
|
/// Max Amount
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
pub max_amount: Option<Amount>,
|
|
|
- /// Amountless
|
|
|
- #[serde(default)]
|
|
|
- pub amountless: bool,
|
|
|
+ /// Options
|
|
|
+ pub options: Option<MeltMethodOptions>,
|
|
|
+}
|
|
|
+
|
|
|
+impl Serialize for MeltMethodSettings {
|
|
|
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
+ where
|
|
|
+ S: Serializer,
|
|
|
+ {
|
|
|
+ let mut num_fields = 3; // method and unit are always present
|
|
|
+ if self.min_amount.is_some() {
|
|
|
+ num_fields += 1;
|
|
|
+ }
|
|
|
+ if self.max_amount.is_some() {
|
|
|
+ num_fields += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut amountless_in_top_level = false;
|
|
|
+ if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
|
|
|
+ if *amountless {
|
|
|
+ num_fields += 1;
|
|
|
+ amountless_in_top_level = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
|
|
|
+
|
|
|
+ state.serialize_field("method", &self.method)?;
|
|
|
+ state.serialize_field("unit", &self.unit)?;
|
|
|
+
|
|
|
+ if let Some(min_amount) = &self.min_amount {
|
|
|
+ state.serialize_field("min_amount", min_amount)?;
|
|
|
+ }
|
|
|
+
|
|
|
+ if let Some(max_amount) = &self.max_amount {
|
|
|
+ state.serialize_field("max_amount", max_amount)?;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If there's an amountless flag in Bolt11 options, add it at the top level
|
|
|
+ if amountless_in_top_level {
|
|
|
+ state.serialize_field("amountless", &true)?;
|
|
|
+ }
|
|
|
+
|
|
|
+ state.end()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct MeltMethodSettingsVisitor;
|
|
|
+
|
|
|
+impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
|
|
|
+ type Value = MeltMethodSettings;
|
|
|
+
|
|
|
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
+ formatter.write_str("a MeltMethodSettings structure")
|
|
|
+ }
|
|
|
+
|
|
|
+ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
|
|
+ where
|
|
|
+ M: MapAccess<'de>,
|
|
|
+ {
|
|
|
+ let mut method: Option<PaymentMethod> = None;
|
|
|
+ let mut unit: Option<CurrencyUnit> = None;
|
|
|
+ let mut min_amount: Option<Amount> = None;
|
|
|
+ let mut max_amount: Option<Amount> = None;
|
|
|
+ let mut amountless: Option<bool> = None;
|
|
|
+
|
|
|
+ while let Some(key) = map.next_key::<String>()? {
|
|
|
+ match key.as_str() {
|
|
|
+ "method" => {
|
|
|
+ if method.is_some() {
|
|
|
+ return Err(de::Error::duplicate_field("method"));
|
|
|
+ }
|
|
|
+ method = Some(map.next_value()?);
|
|
|
+ }
|
|
|
+ "unit" => {
|
|
|
+ if unit.is_some() {
|
|
|
+ return Err(de::Error::duplicate_field("unit"));
|
|
|
+ }
|
|
|
+ unit = Some(map.next_value()?);
|
|
|
+ }
|
|
|
+ "min_amount" => {
|
|
|
+ if min_amount.is_some() {
|
|
|
+ return Err(de::Error::duplicate_field("min_amount"));
|
|
|
+ }
|
|
|
+ min_amount = Some(map.next_value()?);
|
|
|
+ }
|
|
|
+ "max_amount" => {
|
|
|
+ if max_amount.is_some() {
|
|
|
+ return Err(de::Error::duplicate_field("max_amount"));
|
|
|
+ }
|
|
|
+ max_amount = Some(map.next_value()?);
|
|
|
+ }
|
|
|
+ "amountless" => {
|
|
|
+ if amountless.is_some() {
|
|
|
+ return Err(de::Error::duplicate_field("amountless"));
|
|
|
+ }
|
|
|
+ amountless = Some(map.next_value()?);
|
|
|
+ }
|
|
|
+ "options" => {
|
|
|
+ // If there are explicit options, they take precedence, except the amountless
|
|
|
+ // field which we will handle specially
|
|
|
+ let options: Option<MeltMethodOptions> = map.next_value()?;
|
|
|
+
|
|
|
+ if let Some(MeltMethodOptions::Bolt11 {
|
|
|
+ amountless: amountless_from_options,
|
|
|
+ }) = options
|
|
|
+ {
|
|
|
+ // If we already found a top-level amountless, use that instead
|
|
|
+ if amountless.is_none() {
|
|
|
+ amountless = Some(amountless_from_options);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ // Skip unknown fields
|
|
|
+ let _: serde::de::IgnoredAny = map.next_value()?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
|
|
|
+ let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
|
|
|
+
|
|
|
+ // Create options based on the method and the amountless flag
|
|
|
+ let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
|
|
|
+ amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+
|
|
|
+ Ok(MeltMethodSettings {
|
|
|
+ method,
|
|
|
+ unit,
|
|
|
+ min_amount,
|
|
|
+ max_amount,
|
|
|
+ options,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'de> Deserialize<'de> for MeltMethodSettings {
|
|
|
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
+ where
|
|
|
+ D: Deserializer<'de>,
|
|
|
+ {
|
|
|
+ deserializer.deserialize_map(MeltMethodSettingsVisitor)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Mint Method settings options
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
+#[serde(untagged)]
|
|
|
+pub enum MeltMethodOptions {
|
|
|
+ /// Bolt11 Options
|
|
|
+ Bolt11 {
|
|
|
+ /// Mint supports paying bolt11 amountless
|
|
|
+ amountless: bool,
|
|
|
+ },
|
|
|
}
|
|
|
|
|
|
impl Settings {
|
|
@@ -475,3 +354,73 @@ pub struct Settings {
|
|
|
/// Minting disabled
|
|
|
pub disabled: bool,
|
|
|
}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+ use serde_json::{from_str, json, to_string};
|
|
|
+
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_melt_method_settings_top_level_amountless() {
|
|
|
+ // Create JSON with top-level amountless
|
|
|
+ let json_str = r#"{
|
|
|
+ "method": "bolt11",
|
|
|
+ "unit": "sat",
|
|
|
+ "min_amount": 0,
|
|
|
+ "max_amount": 10000,
|
|
|
+ "amountless": true
|
|
|
+ }"#;
|
|
|
+
|
|
|
+ // Deserialize it
|
|
|
+ let settings: MeltMethodSettings = from_str(json_str).unwrap();
|
|
|
+
|
|
|
+ // Check that amountless was correctly moved to options
|
|
|
+ assert_eq!(settings.method, PaymentMethod::Bolt11);
|
|
|
+ assert_eq!(settings.unit, CurrencyUnit::Sat);
|
|
|
+ assert_eq!(settings.min_amount, Some(Amount::from(0)));
|
|
|
+ assert_eq!(settings.max_amount, Some(Amount::from(10000)));
|
|
|
+
|
|
|
+ match settings.options {
|
|
|
+ Some(MeltMethodOptions::Bolt11 { amountless }) => {
|
|
|
+ assert_eq!(amountless, true);
|
|
|
+ }
|
|
|
+ _ => panic!("Expected Bolt11 options with amountless = true"),
|
|
|
+ }
|
|
|
+
|
|
|
+ // Serialize it back
|
|
|
+ let serialized = to_string(&settings).unwrap();
|
|
|
+ let parsed: serde_json::Value = from_str(&serialized).unwrap();
|
|
|
+
|
|
|
+ // Verify the amountless is at the top level
|
|
|
+ assert_eq!(parsed["amountless"], json!(true));
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_both_amountless_locations() {
|
|
|
+ // Create JSON with amountless in both places (top level and in options)
|
|
|
+ let json_str = r#"{
|
|
|
+ "method": "bolt11",
|
|
|
+ "unit": "sat",
|
|
|
+ "min_amount": 0,
|
|
|
+ "max_amount": 10000,
|
|
|
+ "amountless": true,
|
|
|
+ "options": {
|
|
|
+ "amountless": false
|
|
|
+ }
|
|
|
+ }"#;
|
|
|
+
|
|
|
+ // Deserialize it - top level should take precedence
|
|
|
+ let settings: MeltMethodSettings = from_str(json_str).unwrap();
|
|
|
+
|
|
|
+ match settings.options {
|
|
|
+ Some(MeltMethodOptions::Bolt11 { amountless }) => {
|
|
|
+ assert_eq!(
|
|
|
+ amountless, true,
|
|
|
+ "Top-level amountless should take precedence"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ _ => panic!("Expected Bolt11 options with amountless = true"),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|