use std::fmt;
use std::str::FromStr;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
use super::ProofsMethods;
use crate::nut00::KnownMethod;
#[cfg(feature = "mint")]
use crate::quote_id::QuoteId;
use crate::Amount;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown quote state")]
UnknownState,
#[error("Amount Overflow")]
AmountOverflow,
#[error("Unsupported unit")]
UnsupportedUnit,
#[error("Invalid quote id")]
InvalidQuote,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
pub enum QuoteState {
#[default]
Unpaid,
Paid,
Pending,
Unknown,
Failed,
}
impl fmt::Display for QuoteState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Unpaid => write!(f, "UNPAID"),
Self::Paid => write!(f, "PAID"),
Self::Pending => write!(f, "PENDING"),
Self::Unknown => write!(f, "UNKNOWN"),
Self::Failed => write!(f, "FAILED"),
}
}
}
impl FromStr for QuoteState {
type Err = Error;
fn from_str(state: &str) -> Result<Self, Self::Err> {
match state {
"PENDING" => Ok(Self::Pending),
"PAID" => Ok(Self::Paid),
"UNPAID" => Ok(Self::Unpaid),
"UNKNOWN" => Ok(Self::Unknown),
"FAILED" => Ok(Self::Failed),
_ => Err(Error::UnknownState),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MeltRequest<Q> {
quote: Q,
#[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
inputs: Proofs,
outputs: Option<Vec<BlindedMessage>>,
#[serde(default)]
#[cfg_attr(feature = "swagger", schema(value_type = bool))]
prefer_async: bool,
}
#[cfg(feature = "mint")]
impl TryFrom<MeltRequest<String>> for MeltRequest<QuoteId> {
type Error = Error;
fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: QuoteId::from_str(&value.quote).map_err(|_e| Error::InvalidQuote)?,
inputs: value.inputs,
outputs: value.outputs,
prefer_async: value.prefer_async,
})
}
}
impl<Q> MeltRequest<Q> {
pub fn quote_id(&self) -> &Q {
&self.quote
}
pub fn inputs(&self) -> &Proofs {
&self.inputs
}
pub fn inputs_mut(&mut self) -> &mut Proofs {
&mut self.inputs
}
pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
&self.outputs
}
}
impl<Q> MeltRequest<Q>
where
Q: Serialize + DeserializeOwned,
{
pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
Self {
quote,
inputs: inputs.without_dleqs(),
outputs,
prefer_async: false,
}
}
pub fn prefer_async(mut self, prefer_async: bool) -> Self {
self.prefer_async = prefer_async;
self
}
pub fn is_prefer_async(&self) -> bool {
self.prefer_async
}
pub fn quote(&self) -> &Q {
&self.quote
}
pub fn inputs_amount(&self) -> Result<Amount, Error> {
Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
.map_err(|_| Error::AmountOverflow)
}
}
impl<Q> super::nut10::SpendingConditionVerification for MeltRequest<Q>
where
Q: std::fmt::Display,
{
fn inputs(&self) -> &Proofs {
&self.inputs
}
fn sig_all_msg_to_sign(&self) -> String {
let mut msg = String::new();
for proof in &self.inputs {
msg.push_str(&proof.secret.to_string());
msg.push_str(&proof.c.to_hex());
}
if let Some(outputs) = &self.outputs {
for output in outputs {
msg.push_str(&output.amount.to_string());
msg.push_str(&output.blinded_secret.to_hex());
}
}
msg.push_str(&self.quote.to_string());
msg
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MeltMethodSettings {
pub method: PaymentMethod,
pub unit: CurrencyUnit,
pub min_amount: Option<Amount>,
pub max_amount: Option<Amount>,
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; 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 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" => {
let options: Option<MeltMethodOptions> = map.next_value()?;
if let Some(MeltMethodOptions::Bolt11 {
amountless: amountless_from_options,
}) = options
{
if amountless.is_none() {
amountless = Some(amountless_from_options);
}
}
}
_ => {
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"))?;
let options = if method == PaymentMethod::Known(KnownMethod::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)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum MeltMethodOptions {
Bolt11 {
amountless: bool,
},
}
impl Settings {
pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
Self { methods, disabled }
}
pub fn get_settings(
&self,
unit: &CurrencyUnit,
method: &PaymentMethod,
) -> Option<MeltMethodSettings> {
for method_settings in self.methods.iter() {
if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
return Some(method_settings.clone());
}
}
None
}
pub fn remove_settings(
&mut self,
unit: &CurrencyUnit,
method: &PaymentMethod,
) -> Option<MeltMethodSettings> {
self.methods
.iter()
.position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
.map(|index| self.methods.remove(index))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
pub struct Settings {
pub methods: Vec<MeltMethodSettings>,
pub disabled: bool,
}
impl Settings {
pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
self.methods.iter().map(|a| &a.method).collect()
}
pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
self.methods.iter().map(|s| &s.unit).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MeltQuoteCustomRequest {
pub method: String,
pub request: String,
pub unit: CurrencyUnit,
#[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
#[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
pub struct MeltQuoteCustomResponse<Q> {
pub quote: Q,
pub amount: Amount,
pub fee_reserve: Amount,
pub state: QuoteState,
pub expiry: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_preimage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub change: Option<Vec<BlindSignature>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
#[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
pub extra: serde_json::Value,
}
#[cfg(feature = "mint")]
impl<Q: ToString> MeltQuoteCustomResponse<Q> {
pub fn to_string_id(&self) -> MeltQuoteCustomResponse<String> {
MeltQuoteCustomResponse {
quote: self.quote.to_string(),
amount: self.amount,
fee_reserve: self.fee_reserve,
state: self.state,
expiry: self.expiry,
payment_preimage: self.payment_preimage.clone(),
change: self.change.clone(),
request: self.request.clone(),
unit: self.unit.clone(),
extra: self.extra.clone(),
}
}
}
#[cfg(feature = "mint")]
impl From<MeltQuoteCustomResponse<QuoteId>> for MeltQuoteCustomResponse<String> {
fn from(value: MeltQuoteCustomResponse<QuoteId>) -> Self {
Self {
quote: value.quote.to_string(),
amount: value.amount,
fee_reserve: value.fee_reserve,
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_preimage,
change: value.change,
request: value.request,
unit: value.unit,
extra: value.extra,
}
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, json, to_string};
use super::*;
use crate::nut00::KnownMethod;
#[test]
fn test_melt_method_settings_top_level_amountless() {
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"amountless": true
}"#;
let settings: MeltMethodSettings = from_str(json_str).unwrap();
assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::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!(amountless);
}
_ => panic!("Expected Bolt11 options with amountless = true"),
}
let serialized = to_string(&settings).unwrap();
let parsed: serde_json::Value = from_str(&serialized).unwrap();
assert_eq!(parsed["amountless"], json!(true));
}
#[test]
fn test_both_amountless_locations() {
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"amountless": true,
"options": {
"amountless": false
}
}"#;
let settings: MeltMethodSettings = from_str(json_str).unwrap();
match settings.options {
Some(MeltMethodOptions::Bolt11 { amountless }) => {
assert!(amountless, "Top-level amountless should take precedence");
}
_ => panic!("Expected Bolt11 options with amountless = true"),
}
}
}