use std::fmt;
#[cfg(feature = "mint")]
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};
use crate::nut00::KnownMethod;
use crate::nut23::QuoteState;
#[cfg(feature = "mint")]
use crate::quote_id::QuoteId;
#[cfg(feature = "mint")]
use crate::quote_id::QuoteIdError;
use crate::util::serde_helpers::deserialize_empty_string_as_none;
use crate::{Amount, PublicKey};
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown Quote State")]
UnknownState,
#[error("Amount overflow")]
AmountOverflow,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub struct MintRequest<Q> {
#[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
pub quote: Q,
#[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
pub outputs: Vec<BlindedMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
#[cfg(feature = "mint")]
impl TryFrom<MintRequest<String>> for MintRequest<QuoteId> {
type Error = QuoteIdError;
fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
Ok(Self {
quote: QuoteId::from_str(&value.quote)?,
outputs: value.outputs,
signature: value.signature,
})
}
}
impl<Q> MintRequest<Q> {
pub fn total_amount(&self) -> Result<Amount, Error> {
Amount::try_sum(
self.outputs
.iter()
.map(|BlindedMessage { amount, .. }| *amount),
)
.map_err(|_| Error::AmountOverflow)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintResponse {
pub signatures: Vec<BlindSignature>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct MintMethodSettings {
pub method: PaymentMethod,
pub unit: CurrencyUnit,
pub min_amount: Option<Amount>,
pub max_amount: Option<Amount>,
pub options: Option<MintMethodOptions>,
}
impl Serialize for MintMethodSettings {
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 description_in_top_level = false;
if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
if *description {
num_fields += 1;
description_in_top_level = true;
}
}
let mut state = serializer.serialize_struct("MintMethodSettings", 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 description_in_top_level {
state.serialize_field("description", &true)?;
}
state.end()
}
}
struct MintMethodSettingsVisitor;
impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
type Value = MintMethodSettings;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a MintMethodSettings 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 description: 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()?);
}
"description" => {
if description.is_some() {
return Err(de::Error::duplicate_field("description"));
}
description = Some(map.next_value()?);
}
"options" => {
let options: Option<MintMethodOptions> = map.next_value()?;
if let Some(MintMethodOptions::Bolt11 {
description: desc_from_options,
}) = options
{
if description.is_none() {
description = Some(desc_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) {
description.map(|description| MintMethodOptions::Bolt11 { description })
} else {
None
};
Ok(MintMethodSettings {
method,
unit,
min_amount,
max_amount,
options,
})
}
}
impl<'de> Deserialize<'de> for MintMethodSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(MintMethodSettingsVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
#[serde(untagged)]
pub enum MintMethodOptions {
Bolt11 {
description: bool,
},
Custom {},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
pub struct Settings {
pub methods: Vec<MintMethodSettings>,
pub disabled: bool,
}
impl Settings {
pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
Self { methods, disabled }
}
pub fn get_settings(
&self,
unit: &CurrencyUnit,
method: &PaymentMethod,
) -> Option<MintMethodSettings> {
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<MintMethodSettings> {
self.methods
.iter()
.position(|settings| &settings.method == method && &settings.unit == unit)
.map(|index| self.methods.remove(index))
}
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 MintQuoteCustomRequest {
pub amount: Amount,
pub unit: CurrencyUnit,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<PublicKey>,
#[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 MintQuoteCustomResponse<Q> {
pub quote: Q,
pub request: String,
pub amount: Option<Amount>,
pub unit: Option<CurrencyUnit>,
pub state: QuoteState,
pub expiry: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_empty_string_as_none"
)]
pub pubkey: Option<PublicKey>,
#[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> MintQuoteCustomResponse<Q> {
pub fn to_string_id(&self) -> MintQuoteCustomResponse<String> {
MintQuoteCustomResponse {
quote: self.quote.to_string(),
request: self.request.clone(),
amount: self.amount,
state: self.state,
unit: self.unit.clone(),
expiry: self.expiry,
pubkey: self.pubkey,
extra: self.extra.clone(),
}
}
}
#[cfg(feature = "mint")]
impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String> {
fn from(value: MintQuoteCustomResponse<QuoteId>) -> Self {
Self {
quote: value.quote.to_string(),
request: value.request,
amount: value.amount,
unit: value.unit,
expiry: value.expiry,
state: value.state,
pubkey: value.pubkey,
extra: value.extra,
}
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, json, to_string};
use super::*;
use crate::nut00::KnownMethod;
#[test]
fn test_mint_method_settings_top_level_description() {
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"description": true
}"#;
let settings: MintMethodSettings = 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(MintMethodOptions::Bolt11 { description }) => {
assert!(description);
}
_ => panic!("Expected Bolt11 options with description = true"),
}
let serialized = to_string(&settings).unwrap();
let parsed: serde_json::Value = from_str(&serialized).unwrap();
assert_eq!(parsed["description"], json!(true));
}
#[test]
fn test_both_description_locations() {
let json_str = r#"{
"method": "bolt11",
"unit": "sat",
"min_amount": 0,
"max_amount": 10000,
"description": true,
"options": {
"description": false
}
}"#;
let settings: MintMethodSettings = from_str(json_str).unwrap();
match settings.options {
Some(MintMethodOptions::Bolt11 { description }) => {
assert!(description, "Top-level description should take precedence");
}
_ => panic!("Expected Bolt11 options with description = true"),
}
}
}