| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- //! NUT-04: Mint Tokens via Bolt11
- //!
- //! <https://github.com/cashubtc/nuts/blob/main/04.md>
- 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};
- #[cfg(feature = "mint")]
- use crate::quote_id::QuoteId;
- #[cfg(feature = "mint")]
- use crate::quote_id::QuoteIdError;
- use crate::Amount;
- /// NUT04 Error
- #[derive(Debug, Error)]
- pub enum Error {
- /// Unknown Quote State
- #[error("Unknown Quote State")]
- UnknownState,
- /// Amount overflow
- #[error("Amount overflow")]
- AmountOverflow,
- }
- /// Mint request [NUT-04]
- #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- #[serde(bound = "Q: Serialize + DeserializeOwned")]
- pub struct MintRequest<Q> {
- /// Quote id
- #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
- pub quote: Q,
- /// Outputs
- #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
- pub outputs: Vec<BlindedMessage>,
- /// Signature
- #[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> {
- /// Total [`Amount`] of outputs
- pub fn total_amount(&self) -> Result<Amount, Error> {
- Amount::try_sum(
- self.outputs
- .iter()
- .map(|BlindedMessage { amount, .. }| *amount),
- )
- .map_err(|_| Error::AmountOverflow)
- }
- }
- /// Mint response [NUT-04]
- #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- pub struct MintResponse {
- /// Blinded Signatures
- pub signatures: Vec<BlindSignature>,
- }
- /// Mint Method Settings
- #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- pub struct MintMethodSettings {
- /// Payment Method e.g. bolt11
- pub method: PaymentMethod,
- /// Currency Unit e.g. sat
- pub unit: CurrencyUnit,
- /// Min Amount
- pub min_amount: Option<Amount>,
- /// Max Amount
- pub max_amount: Option<Amount>,
- /// Options
- 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; // 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 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 there's a description flag in Bolt11 options, add it at the top level
- 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" => {
- // If there are explicit options, they take precedence, except the description
- // field which we will handle specially
- let options: Option<MintMethodOptions> = map.next_value()?;
- if let Some(MintMethodOptions::Bolt11 {
- description: desc_from_options,
- }) = options
- {
- // If we already found a top-level description, use that instead
- if description.is_none() {
- description = Some(desc_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 description flag
- let options = if method == PaymentMethod::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)
- }
- }
- /// Mint Method settings options
- #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- #[serde(untagged)]
- pub enum MintMethodOptions {
- /// Bolt11 Options
- Bolt11 {
- /// Mint supports setting bolt11 description
- description: bool,
- },
- }
- /// Mint Settings
- #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
- pub struct Settings {
- /// Methods to mint
- pub methods: Vec<MintMethodSettings>,
- /// Minting disabled
- pub disabled: bool,
- }
- impl Settings {
- /// Create new [`Settings`]
- pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
- Self { methods, disabled }
- }
- /// Get [`MintMethodSettings`] for unit method pair
- 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
- }
- /// Remove [`MintMethodSettings`] for unit method pair
- 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))
- }
- /// Supported nut04 methods
- pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
- self.methods.iter().map(|a| &a.method).collect()
- }
- /// Supported nut04 units
- pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
- self.methods.iter().map(|s| &s.unit).collect()
- }
- }
- #[cfg(test)]
- mod tests {
- use serde_json::{from_str, json, to_string};
- use super::*;
- #[test]
- fn test_mint_method_settings_top_level_description() {
- // Create JSON with top-level description
- let json_str = r#"{
- "method": "bolt11",
- "unit": "sat",
- "min_amount": 0,
- "max_amount": 10000,
- "description": true
- }"#;
- // Deserialize it
- let settings: MintMethodSettings = from_str(json_str).unwrap();
- // Check that description 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(MintMethodOptions::Bolt11 { description }) => {
- assert!(description);
- }
- _ => panic!("Expected Bolt11 options with description = true"),
- }
- // Serialize it back
- let serialized = to_string(&settings).unwrap();
- let parsed: serde_json::Value = from_str(&serialized).unwrap();
- // Verify the description is at the top level
- assert_eq!(parsed["description"], json!(true));
- }
- #[test]
- fn test_both_description_locations() {
- // Create JSON with description in both places (top level and in options)
- let json_str = r#"{
- "method": "bolt11",
- "unit": "sat",
- "min_amount": 0,
- "max_amount": 10000,
- "description": true,
- "options": {
- "description": false
- }
- }"#;
- // Deserialize it - top level should take precedence
- 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"),
- }
- }
- }
|