Skip to main content

cdk_ffi/types/
mint.rs

1//! Mint-related FFI types
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7use super::amount::{Amount, CurrencyUnit};
8use super::quote::PaymentMethod;
9use crate::error::FfiError;
10
11/// FFI-compatible Mint URL
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Record)]
13#[serde(transparent)]
14pub struct MintUrl {
15    pub url: String,
16}
17
18impl MintUrl {
19    pub fn new(url: String) -> Result<Self, FfiError> {
20        // Validate URL format
21        url::Url::parse(&url).map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;
22
23        Ok(Self { url })
24    }
25}
26
27impl From<cdk::mint_url::MintUrl> for MintUrl {
28    fn from(mint_url: cdk::mint_url::MintUrl) -> Self {
29        Self {
30            url: mint_url.to_string(),
31        }
32    }
33}
34
35impl TryFrom<MintUrl> for cdk::mint_url::MintUrl {
36    type Error = FfiError;
37
38    fn try_from(mint_url: MintUrl) -> Result<Self, Self::Error> {
39        cdk::mint_url::MintUrl::from_str(&mint_url.url)
40            .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))
41    }
42}
43
44/// FFI-compatible MintVersion
45#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
46pub struct MintVersion {
47    /// Mint Software name
48    pub name: String,
49    /// Mint Version
50    pub version: String,
51}
52
53impl From<cdk::nuts::MintVersion> for MintVersion {
54    fn from(version: cdk::nuts::MintVersion) -> Self {
55        Self {
56            name: version.name,
57            version: version.version,
58        }
59    }
60}
61
62impl From<MintVersion> for cdk::nuts::MintVersion {
63    fn from(version: MintVersion) -> Self {
64        Self {
65            name: version.name,
66            version: version.version,
67        }
68    }
69}
70
71impl MintVersion {
72    /// Convert MintVersion to JSON string
73    pub fn to_json(&self) -> Result<String, FfiError> {
74        Ok(serde_json::to_string(self)?)
75    }
76}
77
78/// Decode MintVersion from JSON string
79#[uniffi::export]
80pub fn decode_mint_version(json: String) -> Result<MintVersion, FfiError> {
81    Ok(serde_json::from_str(&json)?)
82}
83
84/// Encode MintVersion to JSON string
85#[uniffi::export]
86pub fn encode_mint_version(version: MintVersion) -> Result<String, FfiError> {
87    Ok(serde_json::to_string(&version)?)
88}
89
90/// FFI-compatible ContactInfo
91#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
92pub struct ContactInfo {
93    /// Contact Method i.e. nostr
94    pub method: String,
95    /// Contact info i.e. npub...
96    pub info: String,
97}
98
99impl From<cdk::nuts::ContactInfo> for ContactInfo {
100    fn from(contact: cdk::nuts::ContactInfo) -> Self {
101        Self {
102            method: contact.method,
103            info: contact.info,
104        }
105    }
106}
107
108impl From<ContactInfo> for cdk::nuts::ContactInfo {
109    fn from(contact: ContactInfo) -> Self {
110        Self {
111            method: contact.method,
112            info: contact.info,
113        }
114    }
115}
116
117impl ContactInfo {
118    /// Convert ContactInfo to JSON string
119    pub fn to_json(&self) -> Result<String, FfiError> {
120        Ok(serde_json::to_string(self)?)
121    }
122}
123
124/// Decode ContactInfo from JSON string
125#[uniffi::export]
126pub fn decode_contact_info(json: String) -> Result<ContactInfo, FfiError> {
127    Ok(serde_json::from_str(&json)?)
128}
129
130/// Encode ContactInfo to JSON string
131#[uniffi::export]
132pub fn encode_contact_info(info: ContactInfo) -> Result<String, FfiError> {
133    Ok(serde_json::to_string(&info)?)
134}
135
136/// FFI-compatible SupportedSettings
137#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
138#[serde(transparent)]
139pub struct SupportedSettings {
140    /// Setting supported
141    pub supported: bool,
142}
143
144impl From<cdk::nuts::nut06::SupportedSettings> for SupportedSettings {
145    fn from(settings: cdk::nuts::nut06::SupportedSettings) -> Self {
146        Self {
147            supported: settings.supported,
148        }
149    }
150}
151
152impl From<SupportedSettings> for cdk::nuts::nut06::SupportedSettings {
153    fn from(settings: SupportedSettings) -> Self {
154        Self {
155            supported: settings.supported,
156        }
157    }
158}
159
160// -----------------------------
161// NUT-04/05 FFI Types
162// -----------------------------
163
164/// FFI-compatible MintMethodSettings (NUT-04)
165#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
166pub struct MintMethodSettings {
167    pub method: PaymentMethod,
168    pub unit: CurrencyUnit,
169    pub min_amount: Option<Amount>,
170    pub max_amount: Option<Amount>,
171    /// For bolt11, whether mint supports setting invoice description
172    pub description: Option<bool>,
173}
174
175impl From<cdk::nuts::nut04::MintMethodSettings> for MintMethodSettings {
176    fn from(s: cdk::nuts::nut04::MintMethodSettings) -> Self {
177        let description = match s.options {
178            Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) => Some(description),
179            _ => None,
180        };
181        Self {
182            method: s.method.into(),
183            unit: s.unit.into(),
184            min_amount: s.min_amount.map(Into::into),
185            max_amount: s.max_amount.map(Into::into),
186            description,
187        }
188    }
189}
190
191impl TryFrom<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
192    type Error = FfiError;
193
194    fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
195        let options = match s.method {
196            PaymentMethod::Bolt11 => s
197                .description
198                .map(|description| cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }),
199            PaymentMethod::Custom { .. } => Some(cdk::nuts::nut04::MintMethodOptions::Custom {}),
200            _ => None,
201        };
202        Ok(Self {
203            method: s.method.into(),
204            unit: s.unit.into(),
205            min_amount: s.min_amount.map(Into::into),
206            max_amount: s.max_amount.map(Into::into),
207            options,
208        })
209    }
210}
211
212/// FFI-compatible Nut04 Settings
213#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
214pub struct Nut04Settings {
215    pub methods: Vec<MintMethodSettings>,
216    pub disabled: bool,
217}
218
219impl From<cdk::nuts::nut04::Settings> for Nut04Settings {
220    fn from(s: cdk::nuts::nut04::Settings) -> Self {
221        Self {
222            methods: s.methods.into_iter().map(Into::into).collect(),
223            disabled: s.disabled,
224        }
225    }
226}
227
228impl TryFrom<Nut04Settings> for cdk::nuts::nut04::Settings {
229    type Error = FfiError;
230
231    fn try_from(s: Nut04Settings) -> Result<Self, Self::Error> {
232        Ok(Self {
233            methods: s
234                .methods
235                .into_iter()
236                .map(TryInto::try_into)
237                .collect::<Result<_, _>>()?,
238            disabled: s.disabled,
239        })
240    }
241}
242
243/// FFI-compatible MeltMethodSettings (NUT-05)
244#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
245pub struct MeltMethodSettings {
246    pub method: PaymentMethod,
247    pub unit: CurrencyUnit,
248    pub min_amount: Option<Amount>,
249    pub max_amount: Option<Amount>,
250    /// For bolt11, whether mint supports amountless invoices
251    pub amountless: Option<bool>,
252}
253
254impl From<cdk::nuts::nut05::MeltMethodSettings> for MeltMethodSettings {
255    fn from(s: cdk::nuts::nut05::MeltMethodSettings) -> Self {
256        let amountless = match s.options {
257            Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) => Some(amountless),
258            _ => None,
259        };
260        Self {
261            method: s.method.into(),
262            unit: s.unit.into(),
263            min_amount: s.min_amount.map(Into::into),
264            max_amount: s.max_amount.map(Into::into),
265            amountless,
266        }
267    }
268}
269
270impl TryFrom<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
271    type Error = FfiError;
272
273    fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
274        let options = match s.method {
275            PaymentMethod::Bolt11 => s
276                .amountless
277                .map(|amountless| cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }),
278            _ => None,
279        };
280        Ok(Self {
281            method: s.method.into(),
282            unit: s.unit.into(),
283            min_amount: s.min_amount.map(Into::into),
284            max_amount: s.max_amount.map(Into::into),
285            options,
286        })
287    }
288}
289
290/// FFI-compatible Nut05 Settings
291#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
292pub struct Nut05Settings {
293    pub methods: Vec<MeltMethodSettings>,
294    pub disabled: bool,
295}
296
297impl From<cdk::nuts::nut05::Settings> for Nut05Settings {
298    fn from(s: cdk::nuts::nut05::Settings) -> Self {
299        Self {
300            methods: s.methods.into_iter().map(Into::into).collect(),
301            disabled: s.disabled,
302        }
303    }
304}
305
306impl TryFrom<Nut05Settings> for cdk::nuts::nut05::Settings {
307    type Error = FfiError;
308
309    fn try_from(s: Nut05Settings) -> Result<Self, Self::Error> {
310        Ok(Self {
311            methods: s
312                .methods
313                .into_iter()
314                .map(TryInto::try_into)
315                .collect::<Result<_, _>>()?,
316            disabled: s.disabled,
317        })
318    }
319}
320
321/// FFI-compatible ProtectedEndpoint (for auth nuts)
322#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
323pub struct ProtectedEndpoint {
324    /// HTTP method (GET, POST, etc.)
325    pub method: String,
326    /// Endpoint path
327    pub path: String,
328}
329
330/// FFI-compatible ClearAuthSettings (NUT-21)
331#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
332pub struct ClearAuthSettings {
333    /// OpenID Connect discovery URL
334    pub openid_discovery: String,
335    /// OAuth 2.0 client ID
336    pub client_id: String,
337    /// Protected endpoints requiring clear authentication
338    pub protected_endpoints: Vec<ProtectedEndpoint>,
339}
340
341/// FFI-compatible BlindAuthSettings (NUT-22)
342#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
343pub struct BlindAuthSettings {
344    /// Maximum number of blind auth tokens that can be minted per request
345    pub bat_max_mint: u64,
346    /// Protected endpoints requiring blind authentication
347    pub protected_endpoints: Vec<ProtectedEndpoint>,
348}
349
350impl From<cdk::nuts::ClearAuthSettings> for ClearAuthSettings {
351    fn from(settings: cdk::nuts::ClearAuthSettings) -> Self {
352        Self {
353            openid_discovery: settings.openid_discovery,
354            client_id: settings.client_id,
355            protected_endpoints: settings
356                .protected_endpoints
357                .into_iter()
358                .map(Into::into)
359                .collect(),
360        }
361    }
362}
363
364impl TryFrom<ClearAuthSettings> for cdk::nuts::ClearAuthSettings {
365    type Error = FfiError;
366
367    fn try_from(settings: ClearAuthSettings) -> Result<Self, Self::Error> {
368        Ok(Self {
369            openid_discovery: settings.openid_discovery,
370            client_id: settings.client_id,
371            protected_endpoints: settings
372                .protected_endpoints
373                .into_iter()
374                .map(|e| e.try_into())
375                .collect::<Result<Vec<_>, _>>()?,
376        })
377    }
378}
379
380impl From<cdk::nuts::BlindAuthSettings> for BlindAuthSettings {
381    fn from(settings: cdk::nuts::BlindAuthSettings) -> Self {
382        Self {
383            bat_max_mint: settings.bat_max_mint,
384            protected_endpoints: settings
385                .protected_endpoints
386                .into_iter()
387                .map(Into::into)
388                .collect(),
389        }
390    }
391}
392
393impl TryFrom<BlindAuthSettings> for cdk::nuts::BlindAuthSettings {
394    type Error = FfiError;
395
396    fn try_from(settings: BlindAuthSettings) -> Result<Self, Self::Error> {
397        Ok(Self {
398            bat_max_mint: settings.bat_max_mint,
399            protected_endpoints: settings
400                .protected_endpoints
401                .into_iter()
402                .map(|e| e.try_into())
403                .collect::<Result<Vec<_>, _>>()?,
404        })
405    }
406}
407
408/// FFI-compatible Nut29Settings (NUT-29)
409#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record, Default)]
410pub struct Nut29Settings {
411    /// Maximum number of quotes allowed in a single batch
412    pub max_batch_size: Option<u64>,
413    /// Supported payment methods for batch minting
414    pub methods: Option<Vec<String>>,
415}
416
417impl From<cdk::nuts::nut29::Settings> for Nut29Settings {
418    fn from(settings: cdk::nuts::nut29::Settings) -> Self {
419        Self {
420            max_batch_size: settings.max_batch_size,
421            methods: settings.methods,
422        }
423    }
424}
425
426impl From<Nut29Settings> for cdk::nuts::nut29::Settings {
427    fn from(settings: Nut29Settings) -> Self {
428        Self {
429            max_batch_size: settings.max_batch_size,
430            methods: settings.methods,
431        }
432    }
433}
434
435impl From<cdk::nuts::ProtectedEndpoint> for ProtectedEndpoint {
436    fn from(endpoint: cdk::nuts::ProtectedEndpoint) -> Self {
437        Self {
438            method: match endpoint.method {
439                cdk::nuts::Method::Get => "GET".to_string(),
440                cdk::nuts::Method::Post => "POST".to_string(),
441            },
442            path: endpoint.path.to_string(),
443        }
444    }
445}
446
447impl TryFrom<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
448    type Error = FfiError;
449
450    fn try_from(endpoint: ProtectedEndpoint) -> Result<Self, Self::Error> {
451        let method = match endpoint.method.as_str() {
452            "GET" => cdk::nuts::Method::Get,
453            "POST" => cdk::nuts::Method::Post,
454            _ => {
455                return Err(FfiError::internal(format!(
456                    "Invalid HTTP method: {}. Only GET and POST are supported",
457                    endpoint.method
458                )))
459            }
460        };
461
462        let route_path = endpoint
463            .path
464            .parse()
465            .map_err(|err| FfiError::internal(format!("Unknown route path: {err}")))?;
466
467        Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path))
468    }
469}
470
471/// FFI-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings)
472#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
473pub struct Nuts {
474    /// NUT04 Settings
475    pub nut04: Nut04Settings,
476    /// NUT05 Settings
477    pub nut05: Nut05Settings,
478    /// NUT07 Settings - Token state check
479    pub nut07_supported: bool,
480    /// NUT08 Settings - Lightning fee return
481    pub nut08_supported: bool,
482    /// NUT09 Settings - Restore signature
483    pub nut09_supported: bool,
484    /// NUT10 Settings - Spending conditions
485    pub nut10_supported: bool,
486    /// NUT11 Settings - Pay to Public Key Hash
487    pub nut11_supported: bool,
488    /// NUT12 Settings - DLEQ proofs
489    pub nut12_supported: bool,
490    /// NUT14 Settings - Hashed Time Locked Contracts
491    pub nut14_supported: bool,
492    /// NUT20 Settings - Web sockets
493    pub nut20_supported: bool,
494    /// NUT21 Settings - Clear authentication
495    pub nut21: Option<ClearAuthSettings>,
496    /// NUT22 Settings - Blind authentication
497    pub nut22: Option<BlindAuthSettings>,
498    /// NUT29 Settings - Batch minting
499    pub nut29: Nut29Settings,
500    /// Supported currency units for minting
501    pub mint_units: Vec<CurrencyUnit>,
502    /// Supported currency units for melting
503    pub melt_units: Vec<CurrencyUnit>,
504}
505
506impl From<cdk::nuts::Nuts> for Nuts {
507    fn from(nuts: cdk::nuts::Nuts) -> Self {
508        let mint_units = nuts
509            .supported_mint_units()
510            .into_iter()
511            .map(|u| u.clone().into())
512            .collect();
513        let melt_units = nuts
514            .supported_melt_units()
515            .into_iter()
516            .map(|u| u.clone().into())
517            .collect();
518
519        Self {
520            nut04: nuts.nut04.clone().into(),
521            nut05: nuts.nut05.clone().into(),
522            nut07_supported: nuts.nut07.supported,
523            nut08_supported: nuts.nut08.supported,
524            nut09_supported: nuts.nut09.supported,
525            nut10_supported: nuts.nut10.supported,
526            nut11_supported: nuts.nut11.supported,
527            nut12_supported: nuts.nut12.supported,
528            nut14_supported: nuts.nut14.supported,
529            nut20_supported: nuts.nut20.supported,
530            nut21: nuts.nut21.map(Into::into),
531            nut22: nuts.nut22.map(Into::into),
532            nut29: nuts.nut29.into(),
533            mint_units,
534            melt_units,
535        }
536    }
537}
538
539impl TryFrom<Nuts> for cdk::nuts::Nuts {
540    type Error = FfiError;
541
542    fn try_from(n: Nuts) -> Result<Self, Self::Error> {
543        Ok(Self {
544            nut04: n.nut04.try_into()?,
545            nut05: n.nut05.try_into()?,
546            nut07: cdk::nuts::nut06::SupportedSettings {
547                supported: n.nut07_supported,
548            },
549            nut08: cdk::nuts::nut06::SupportedSettings {
550                supported: n.nut08_supported,
551            },
552            nut09: cdk::nuts::nut06::SupportedSettings {
553                supported: n.nut09_supported,
554            },
555            nut10: cdk::nuts::nut06::SupportedSettings {
556                supported: n.nut10_supported,
557            },
558            nut11: cdk::nuts::nut06::SupportedSettings {
559                supported: n.nut11_supported,
560            },
561            nut12: cdk::nuts::nut06::SupportedSettings {
562                supported: n.nut12_supported,
563            },
564            nut14: cdk::nuts::nut06::SupportedSettings {
565                supported: n.nut14_supported,
566            },
567            nut15: Default::default(),
568            nut17: Default::default(),
569            nut19: Default::default(),
570            nut20: cdk::nuts::nut06::SupportedSettings {
571                supported: n.nut20_supported,
572            },
573            nut21: n.nut21.map(|s| s.try_into()).transpose()?,
574            nut22: n.nut22.map(|s| s.try_into()).transpose()?,
575            nut29: n.nut29.into(),
576        })
577    }
578}
579
580impl Nuts {
581    /// Convert Nuts to JSON string
582    pub fn to_json(&self) -> Result<String, FfiError> {
583        Ok(serde_json::to_string(self)?)
584    }
585}
586
587/// Decode Nuts from JSON string
588#[uniffi::export]
589pub fn decode_nuts(json: String) -> Result<Nuts, FfiError> {
590    Ok(serde_json::from_str(&json)?)
591}
592
593/// Encode Nuts to JSON string
594#[uniffi::export]
595pub fn encode_nuts(nuts: Nuts) -> Result<String, FfiError> {
596    Ok(serde_json::to_string(&nuts)?)
597}
598
599/// FFI-compatible MintInfo
600#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
601pub struct MintInfo {
602    /// name of the mint and should be recognizable
603    pub name: Option<String>,
604    /// hex pubkey of the mint
605    pub pubkey: Option<String>,
606    /// implementation name and the version running
607    pub version: Option<MintVersion>,
608    /// short description of the mint
609    pub description: Option<String>,
610    /// long description
611    pub description_long: Option<String>,
612    /// Contact info
613    pub contact: Option<Vec<ContactInfo>>,
614    /// shows which NUTs the mint supports
615    pub nuts: Nuts,
616    /// Mint's icon URL
617    pub icon_url: Option<String>,
618    /// Mint's endpoint URLs
619    pub urls: Option<Vec<String>>,
620    /// message of the day that the wallet must display to the user
621    pub motd: Option<String>,
622    /// server unix timestamp
623    pub time: Option<u64>,
624    /// terms of url service of the mint
625    pub tos_url: Option<String>,
626}
627
628impl From<cdk::nuts::MintInfo> for MintInfo {
629    fn from(info: cdk::nuts::MintInfo) -> Self {
630        Self {
631            name: info.name,
632            pubkey: info.pubkey.map(|p| p.to_string()),
633            version: info.version.map(Into::into),
634            description: info.description,
635            description_long: info.description_long,
636            contact: info
637                .contact
638                .map(|contacts| contacts.into_iter().map(Into::into).collect()),
639            nuts: info.nuts.into(),
640            icon_url: info.icon_url,
641            urls: info.urls,
642            motd: info.motd,
643            time: info.time,
644            tos_url: info.tos_url,
645        }
646    }
647}
648
649impl TryFrom<MintInfo> for cdk::nuts::MintInfo {
650    type Error = FfiError;
651
652    fn try_from(info: MintInfo) -> Result<Self, Self::Error> {
653        Ok(Self {
654            name: info.name,
655            pubkey: info.pubkey.and_then(|p| p.parse().ok()),
656            version: info.version.map(Into::into),
657            description: info.description,
658            description_long: info.description_long,
659            contact: info
660                .contact
661                .map(|contacts| contacts.into_iter().map(Into::into).collect()),
662            nuts: info.nuts.try_into()?,
663            icon_url: info.icon_url,
664            urls: info.urls,
665            motd: info.motd,
666            time: info.time,
667            tos_url: info.tos_url,
668        })
669    }
670}
671
672impl MintInfo {
673    /// Convert MintInfo to JSON string
674    pub fn to_json(&self) -> Result<String, FfiError> {
675        Ok(serde_json::to_string(self)?)
676    }
677}
678
679/// Decode MintInfo from JSON string
680#[uniffi::export]
681pub fn decode_mint_info(json: String) -> Result<MintInfo, FfiError> {
682    Ok(serde_json::from_str(&json)?)
683}
684
685/// Encode MintInfo to JSON string
686#[uniffi::export]
687pub fn encode_mint_info(info: MintInfo) -> Result<String, FfiError> {
688    Ok(serde_json::to_string(&info)?)
689}
690#[cfg(test)]
691mod tests {
692    use cdk::nuts::nut00::{KnownMethod, PaymentMethod as NutPaymentMethod};
693
694    use super::*;
695
696    /// Helper function to create a sample cdk::nuts::Nuts for testing
697    fn create_sample_cdk_nuts() -> cdk::nuts::Nuts {
698        cdk::nuts::Nuts {
699            nut04: cdk::nuts::nut04::Settings {
700                methods: vec![cdk::nuts::nut04::MintMethodSettings {
701                    method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
702                    unit: cdk::nuts::CurrencyUnit::Sat,
703                    min_amount: Some(cdk::Amount::from(1)),
704                    max_amount: Some(cdk::Amount::from(100000)),
705                    options: Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 {
706                        description: true,
707                    }),
708                }],
709                disabled: false,
710            },
711            nut05: cdk::nuts::nut05::Settings {
712                methods: vec![cdk::nuts::nut05::MeltMethodSettings {
713                    method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
714                    unit: cdk::nuts::CurrencyUnit::Sat,
715                    min_amount: Some(cdk::Amount::from(1)),
716                    max_amount: Some(cdk::Amount::from(100000)),
717                    options: Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless: true }),
718                }],
719                disabled: false,
720            },
721            nut07: cdk::nuts::nut06::SupportedSettings { supported: true },
722            nut08: cdk::nuts::nut06::SupportedSettings { supported: true },
723            nut09: cdk::nuts::nut06::SupportedSettings { supported: false },
724            nut10: cdk::nuts::nut06::SupportedSettings { supported: true },
725            nut11: cdk::nuts::nut06::SupportedSettings { supported: true },
726            nut12: cdk::nuts::nut06::SupportedSettings { supported: true },
727            nut14: cdk::nuts::nut06::SupportedSettings { supported: false },
728            nut15: Default::default(),
729            nut17: Default::default(),
730            nut19: Default::default(),
731            nut20: cdk::nuts::nut06::SupportedSettings { supported: true },
732            nut21: Some(cdk::nuts::ClearAuthSettings {
733                openid_discovery: "https://example.com/.well-known/openid-configuration"
734                    .to_string(),
735                client_id: "test-client".to_string(),
736                protected_endpoints: vec![cdk::nuts::ProtectedEndpoint::new(
737                    cdk::nuts::Method::Post,
738                    cdk::nuts::RoutePath::Swap,
739                )],
740            }),
741            nut22: Some(cdk::nuts::BlindAuthSettings {
742                bat_max_mint: 100,
743                protected_endpoints: vec![cdk::nuts::ProtectedEndpoint::new(
744                    cdk::nuts::Method::Post,
745                    cdk::nuts::RoutePath::Mint(
746                        NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
747                    ),
748                )],
749            }),
750            nut29: Default::default(),
751        }
752    }
753
754    #[test]
755    fn test_nuts_from_cdk_to_ffi() {
756        let cdk_nuts = create_sample_cdk_nuts();
757        let ffi_nuts: Nuts = cdk_nuts.clone().into();
758
759        // Verify NUT04 settings
760        assert!(!ffi_nuts.nut04.disabled);
761        assert_eq!(ffi_nuts.nut04.methods.len(), 1);
762        assert_eq!(ffi_nuts.nut04.methods[0].description, Some(true));
763
764        // Verify NUT05 settings
765        assert!(!ffi_nuts.nut05.disabled);
766        assert_eq!(ffi_nuts.nut05.methods.len(), 1);
767        assert_eq!(ffi_nuts.nut05.methods[0].amountless, Some(true));
768
769        // Verify supported flags
770        assert!(ffi_nuts.nut07_supported);
771        assert!(ffi_nuts.nut08_supported);
772        assert!(!ffi_nuts.nut09_supported);
773        assert!(ffi_nuts.nut10_supported);
774        assert!(ffi_nuts.nut11_supported);
775        assert!(ffi_nuts.nut12_supported);
776        assert!(!ffi_nuts.nut14_supported);
777        assert!(ffi_nuts.nut20_supported);
778
779        // Verify auth settings
780        assert!(ffi_nuts.nut21.is_some());
781        let nut21 = ffi_nuts.nut21.as_ref().unwrap();
782        assert_eq!(
783            nut21.openid_discovery,
784            "https://example.com/.well-known/openid-configuration"
785        );
786        assert_eq!(nut21.client_id, "test-client");
787        assert_eq!(nut21.protected_endpoints.len(), 1);
788
789        assert!(ffi_nuts.nut22.is_some());
790        let nut22 = ffi_nuts.nut22.as_ref().unwrap();
791        assert_eq!(nut22.bat_max_mint, 100);
792        assert_eq!(nut22.protected_endpoints.len(), 1);
793
794        // Verify units
795        assert!(!ffi_nuts.mint_units.is_empty());
796        assert!(!ffi_nuts.melt_units.is_empty());
797    }
798
799    #[test]
800    fn test_nuts_round_trip_conversion() {
801        let original_cdk_nuts = create_sample_cdk_nuts();
802
803        // Convert cdk -> ffi -> cdk
804        let ffi_nuts: Nuts = original_cdk_nuts.clone().into();
805        let converted_back: cdk::nuts::Nuts = ffi_nuts.try_into().unwrap();
806
807        // Verify all supported flags match
808        assert_eq!(
809            original_cdk_nuts.nut07.supported,
810            converted_back.nut07.supported
811        );
812        assert_eq!(
813            original_cdk_nuts.nut08.supported,
814            converted_back.nut08.supported
815        );
816        assert_eq!(
817            original_cdk_nuts.nut09.supported,
818            converted_back.nut09.supported
819        );
820        assert_eq!(
821            original_cdk_nuts.nut10.supported,
822            converted_back.nut10.supported
823        );
824        assert_eq!(
825            original_cdk_nuts.nut11.supported,
826            converted_back.nut11.supported
827        );
828        assert_eq!(
829            original_cdk_nuts.nut12.supported,
830            converted_back.nut12.supported
831        );
832        assert_eq!(
833            original_cdk_nuts.nut14.supported,
834            converted_back.nut14.supported
835        );
836        assert_eq!(
837            original_cdk_nuts.nut20.supported,
838            converted_back.nut20.supported
839        );
840
841        // Verify NUT04 settings
842        assert_eq!(
843            original_cdk_nuts.nut04.disabled,
844            converted_back.nut04.disabled
845        );
846        assert_eq!(
847            original_cdk_nuts.nut04.methods.len(),
848            converted_back.nut04.methods.len()
849        );
850
851        // Verify NUT05 settings
852        assert_eq!(
853            original_cdk_nuts.nut05.disabled,
854            converted_back.nut05.disabled
855        );
856        assert_eq!(
857            original_cdk_nuts.nut05.methods.len(),
858            converted_back.nut05.methods.len()
859        );
860
861        // Verify auth settings presence
862        assert_eq!(
863            original_cdk_nuts.nut21.is_some(),
864            converted_back.nut21.is_some()
865        );
866        assert_eq!(
867            original_cdk_nuts.nut22.is_some(),
868            converted_back.nut22.is_some()
869        );
870    }
871
872    #[test]
873    fn test_nuts_without_auth() {
874        let cdk_nuts = cdk::nuts::Nuts {
875            nut04: Default::default(),
876            nut05: Default::default(),
877            nut07: cdk::nuts::nut06::SupportedSettings { supported: true },
878            nut08: cdk::nuts::nut06::SupportedSettings { supported: false },
879            nut09: cdk::nuts::nut06::SupportedSettings { supported: false },
880            nut10: cdk::nuts::nut06::SupportedSettings { supported: false },
881            nut11: cdk::nuts::nut06::SupportedSettings { supported: false },
882            nut12: cdk::nuts::nut06::SupportedSettings { supported: false },
883            nut14: cdk::nuts::nut06::SupportedSettings { supported: false },
884            nut15: Default::default(),
885            nut17: Default::default(),
886            nut19: Default::default(),
887            nut20: cdk::nuts::nut06::SupportedSettings { supported: false },
888            nut21: None,
889            nut22: None,
890            nut29: Default::default(),
891        };
892
893        let ffi_nuts: Nuts = cdk_nuts.into();
894
895        assert!(ffi_nuts.nut21.is_none());
896        assert!(ffi_nuts.nut22.is_none());
897        assert!(ffi_nuts.nut07_supported);
898        assert!(!ffi_nuts.nut08_supported);
899    }
900
901    #[test]
902    fn test_ffi_nuts_to_cdk_with_defaults() {
903        let ffi_nuts = Nuts {
904            nut04: Nut04Settings {
905                methods: vec![],
906                disabled: true,
907            },
908            nut05: Nut05Settings {
909                methods: vec![],
910                disabled: true,
911            },
912            nut07_supported: false,
913            nut08_supported: false,
914            nut09_supported: false,
915            nut10_supported: false,
916            nut11_supported: false,
917            nut12_supported: false,
918            nut14_supported: false,
919            nut20_supported: false,
920            nut21: None,
921            nut22: None,
922            nut29: Default::default(),
923            mint_units: vec![],
924            melt_units: vec![],
925        };
926
927        let cdk_nuts: Result<cdk::nuts::Nuts, _> = ffi_nuts.try_into();
928        assert!(cdk_nuts.is_ok());
929
930        let cdk_nuts = cdk_nuts.unwrap();
931        assert!(!cdk_nuts.nut07.supported);
932        assert!(!cdk_nuts.nut08.supported);
933        assert!(cdk_nuts.nut21.is_none());
934        assert!(cdk_nuts.nut22.is_none());
935
936        // Verify default values for nuts not included in FFI
937        assert_eq!(cdk_nuts.nut17.supported.len(), 0);
938    }
939
940    #[test]
941    fn test_nuts_serialization() {
942        let cdk_nuts = create_sample_cdk_nuts();
943        let ffi_nuts: Nuts = cdk_nuts.into();
944
945        // Test JSON serialization
946        let json = ffi_nuts.to_json();
947        assert!(json.is_ok());
948
949        let json_str = json.unwrap();
950        assert!(json_str.contains("nut04"));
951        assert!(json_str.contains("nut05"));
952
953        // Test deserialization
954        let decoded: Result<Nuts, _> = serde_json::from_str(&json_str);
955        assert!(decoded.is_ok());
956
957        let decoded_nuts = decoded.unwrap();
958        assert_eq!(decoded_nuts.nut07_supported, ffi_nuts.nut07_supported);
959        assert_eq!(decoded_nuts.nut08_supported, ffi_nuts.nut08_supported);
960    }
961
962    #[test]
963    fn test_nuts_multiple_units() {
964        let mut cdk_nuts = create_sample_cdk_nuts();
965
966        // Add multiple payment methods to test unit collection
967        cdk_nuts
968            .nut04
969            .methods
970            .push(cdk::nuts::nut04::MintMethodSettings {
971                method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
972                unit: cdk::nuts::CurrencyUnit::Msat,
973                min_amount: Some(cdk::Amount::from(1)),
974                max_amount: Some(cdk::Amount::from(100000)),
975                options: None,
976            });
977
978        cdk_nuts
979            .nut05
980            .methods
981            .push(cdk::nuts::nut05::MeltMethodSettings {
982                method: cdk::nuts::PaymentMethod::Known(KnownMethod::Bolt11),
983                unit: cdk::nuts::CurrencyUnit::Usd,
984                min_amount: None,
985                max_amount: None,
986                options: None,
987            });
988
989        let ffi_nuts: Nuts = cdk_nuts.into();
990
991        // Should have collected multiple units
992        assert!(!ffi_nuts.mint_units.is_empty());
993        assert!(!ffi_nuts.melt_units.is_empty());
994    }
995
996    #[test]
997    fn test_protected_endpoint_conversion() {
998        let cdk_endpoint =
999            cdk::nuts::ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::Swap);
1000
1001        let ffi_endpoint: ProtectedEndpoint = cdk_endpoint.into();
1002
1003        assert_eq!(ffi_endpoint.method, "POST");
1004        assert_eq!(ffi_endpoint.path, "/v1/swap");
1005
1006        // Test round-trip
1007        let converted_back: Result<cdk::nuts::ProtectedEndpoint, _> = ffi_endpoint.try_into();
1008        assert!(converted_back.is_ok());
1009    }
1010
1011    #[test]
1012    fn test_invalid_protected_endpoint_method() {
1013        let invalid_endpoint = ProtectedEndpoint {
1014            method: "INVALID".to_string(),
1015            path: "/v1/swap".to_string(),
1016        };
1017
1018        let result: Result<cdk::nuts::ProtectedEndpoint, _> = invalid_endpoint.try_into();
1019        assert!(result.is_err());
1020    }
1021
1022    #[test]
1023    fn test_invalid_protected_endpoint_path() {
1024        let invalid_endpoint = ProtectedEndpoint {
1025            method: "POST".to_string(),
1026            path: "/invalid/path".to_string(),
1027        };
1028
1029        let result: Result<cdk::nuts::ProtectedEndpoint, _> = invalid_endpoint.try_into();
1030        assert!(result.is_err());
1031    }
1032
1033    #[test]
1034    fn test_protected_endpoint_custom_payment_method_path() {
1035        let endpoint = ProtectedEndpoint {
1036            method: "POST".to_string(),
1037            path: "/v1/mint/custom-method".to_string(),
1038        };
1039
1040        let converted: cdk::nuts::ProtectedEndpoint = endpoint
1041            .try_into()
1042            .expect("custom payment method routes should be accepted");
1043        assert_eq!(
1044            converted.path,
1045            cdk::nuts::RoutePath::Mint("custom-method".to_string())
1046        );
1047    }
1048
1049    #[test]
1050    fn test_mint_info_unknown_endpoint_returns_error() {
1051        let ffi_mint_info = MintInfo {
1052            name: None,
1053            pubkey: None,
1054            version: None,
1055            description: None,
1056            description_long: None,
1057            contact: None,
1058            nuts: Nuts {
1059                nut04: Nut04Settings {
1060                    methods: vec![],
1061                    disabled: false,
1062                },
1063                nut05: Nut05Settings {
1064                    methods: vec![],
1065                    disabled: false,
1066                },
1067                nut07_supported: true,
1068                nut08_supported: false,
1069                nut09_supported: false,
1070                nut10_supported: false,
1071                nut11_supported: false,
1072                nut12_supported: true,
1073                nut14_supported: false,
1074                nut20_supported: false,
1075                nut21: None,
1076                nut22: Some(BlindAuthSettings {
1077                    bat_max_mint: 10,
1078                    protected_endpoints: vec![ProtectedEndpoint {
1079                        method: "POST".to_string(),
1080                        path: "/v1/unknown-custom-endpoint".to_string(),
1081                    }],
1082                }),
1083                nut29: Nut29Settings::default(),
1084                mint_units: vec![],
1085                melt_units: vec![],
1086            },
1087            icon_url: None,
1088            urls: None,
1089            motd: None,
1090            time: None,
1091            tos_url: None,
1092        };
1093
1094        let result = cdk::nuts::MintInfo::try_from(ffi_mint_info);
1095        assert!(result.is_err());
1096    }
1097}