Skip to main content

cdk/mint/
builder.rs

1//! Mint Builder
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use bitcoin::bip32::DerivationPath;
7use cdk_common::database::{DynMintAuthDatabase, DynMintDatabase, MintKeysDatabase};
8use cdk_common::error::Error;
9use cdk_common::nut00::KnownMethod;
10use cdk_common::nut04::MintMethodOptions;
11use cdk_common::nut05::MeltMethodOptions;
12use cdk_common::payment::DynMintPayment;
13use cdk_common::{nut21, nut22};
14use cdk_signatory::signatory::{RotateKeyArguments, Signatory};
15
16use super::nut17::SupportedMethods;
17use super::nut19::{self, CachedEndpoint};
18use super::Nuts;
19use crate::amount::Amount;
20use crate::cdk_database;
21use crate::mint::Mint;
22use crate::nuts::{
23    AuthRequired, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings,
24    MintVersion, MppMethodSettings, PaymentMethod, ProtectedEndpoint,
25};
26use crate::types::PaymentProcessorKey;
27
28/// Configuration for a mint unit (keyset)
29#[derive(Debug, Clone)]
30pub struct UnitConfig {
31    /// List of amounts to support (e.g., [1, 2, 4, 8, 16, 32])
32    pub amounts: Vec<u64>,
33    /// Input fee in parts per thousand
34    pub input_fee_ppk: u64,
35}
36
37impl Default for UnitConfig {
38    fn default() -> Self {
39        Self {
40            amounts: (0..32).map(|i| 2_u64.pow(i)).collect(),
41            input_fee_ppk: 0,
42        }
43    }
44}
45
46/// Describes an extra keyset rotation to perform during mint build.
47/// Used to create inactive/expired keysets for testing.
48#[derive(Debug, Clone)]
49pub struct KeysetRotation {
50    /// Currency unit for this rotation
51    pub unit: CurrencyUnit,
52    /// Amounts for the keyset
53    pub amounts: Vec<u64>,
54    /// Input fee
55    pub input_fee_ppk: u64,
56    /// Whether to use keyset V2 (Version01) or V1 (Version00)
57    pub use_keyset_v2: bool,
58    /// Optional expiry timestamp (unix seconds)
59    pub final_expiry: Option<u64>,
60}
61
62/// Cashu Mint Builder
63pub struct MintBuilder {
64    mint_info: MintInfo,
65    localstore: DynMintDatabase,
66    auth_localstore: Option<DynMintAuthDatabase>,
67    clear_auth_endpoints: Vec<ProtectedEndpoint>,
68    blind_auth_endpoints: Vec<ProtectedEndpoint>,
69    blind_auth_configured: bool,
70    payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
71    supported_units: HashMap<CurrencyUnit, (u64, Vec<u64>)>,
72    custom_paths: HashMap<CurrencyUnit, DerivationPath>,
73    use_keyset_v2: Option<bool>,
74    keyset_rotations: Vec<KeysetRotation>,
75    max_inputs: usize,
76    max_outputs: usize,
77    max_batch_size: Option<u64>,
78}
79
80impl std::fmt::Debug for MintBuilder {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("MintBuilder")
83            .field("mint_info", &self.mint_info)
84            .field("supported_units", &self.supported_units)
85            .finish_non_exhaustive()
86    }
87}
88
89impl MintBuilder {
90    /// New [`MintBuilder`]
91    pub fn new(localstore: DynMintDatabase) -> MintBuilder {
92        let mint_info = MintInfo {
93            nuts: Nuts::new()
94                .nut07(true)
95                .nut08(true)
96                .nut09(true)
97                .nut10(true)
98                .nut11(true)
99                .nut12(true)
100                .nut14(true)
101                .nut20(true)
102                .nut29(cdk_common::nut29::Settings::default()),
103            ..Default::default()
104        };
105
106        MintBuilder {
107            mint_info,
108            localstore,
109            auth_localstore: None,
110            clear_auth_endpoints: Vec::new(),
111            blind_auth_endpoints: Vec::new(),
112            blind_auth_configured: false,
113            payment_processors: HashMap::new(),
114            supported_units: HashMap::new(),
115            custom_paths: HashMap::new(),
116            use_keyset_v2: None,
117            keyset_rotations: Vec::new(),
118            max_inputs: 1000,
119            max_outputs: 1000,
120            max_batch_size: None,
121        }
122    }
123
124    /// Set use keyset v2
125    pub fn with_keyset_v2(mut self, use_keyset_v2: Option<bool>) -> Self {
126        self.use_keyset_v2 = use_keyset_v2;
127        self
128    }
129
130    /// Add a keyset rotation to execute during build.
131    /// Used to create inactive/expired keysets for testing.
132    pub fn with_keyset_rotation(mut self, rotation: KeysetRotation) -> Self {
133        self.keyset_rotations.push(rotation);
134        self
135    }
136
137    /// Set clear auth settings
138    pub fn with_auth(
139        mut self,
140        auth_localstore: DynMintAuthDatabase,
141        openid_discovery: String,
142        client_id: String,
143        protected_endpoints: Vec<ProtectedEndpoint>,
144    ) -> Self {
145        self.auth_localstore = Some(auth_localstore);
146        self.clear_auth_endpoints = protected_endpoints.clone();
147        self.mint_info.nuts.nut21 = Some(nut21::Settings::new(
148            openid_discovery,
149            client_id,
150            protected_endpoints,
151        ));
152        self
153    }
154
155    /// Initialize builder's MintInfo from the database if present.
156    /// If not present or parsing fails, keeps the current MintInfo.
157    pub async fn init_from_db_if_present(&mut self) -> Result<(), cdk_database::Error> {
158        // Attempt to read existing mint_info from the KV store
159        let bytes_opt = self
160            .localstore
161            .kv_read(
162                super::CDK_MINT_PRIMARY_NAMESPACE,
163                super::CDK_MINT_CONFIG_SECONDARY_NAMESPACE,
164                super::CDK_MINT_CONFIG_KV_KEY,
165            )
166            .await?;
167
168        if let Some(bytes) = bytes_opt {
169            if let Ok(info) = serde_json::from_slice::<MintInfo>(&bytes) {
170                self.mint_info = info;
171            } else {
172                // If parsing fails, leave the current builder state untouched
173                tracing::warn!("Failed to parse existing mint_info from DB; using builder state");
174            }
175        }
176
177        Ok(())
178    }
179
180    /// Set blind auth settings
181    pub fn with_blind_auth(
182        mut self,
183        bat_max_mint: u64,
184        protected_endpoints: Vec<ProtectedEndpoint>,
185    ) -> Self {
186        let mut nuts = self.mint_info.nuts;
187
188        self.blind_auth_endpoints = protected_endpoints.clone();
189        self.blind_auth_configured = true;
190
191        nuts.nut22 = Some(nut22::Settings::new(bat_max_mint, protected_endpoints));
192
193        self.mint_info.nuts = nuts;
194
195        self
196    }
197
198    /// Set mint info
199    pub fn with_mint_info(mut self, mint_info: MintInfo) -> Self {
200        self.mint_info = mint_info;
201        self
202    }
203
204    /// Set name
205    pub fn with_name(mut self, name: String) -> Self {
206        self.mint_info.name = Some(name);
207        self
208    }
209
210    /// Set initial mint URLs
211    pub fn with_urls(mut self, urls: Vec<String>) -> Self {
212        self.mint_info.urls = Some(urls);
213        self
214    }
215
216    /// Set icon url
217    pub fn with_icon_url(mut self, icon_url: String) -> Self {
218        self.mint_info.icon_url = Some(icon_url);
219        self
220    }
221
222    /// Set icon url
223    pub fn with_motd(mut self, motd: String) -> Self {
224        self.mint_info.motd = Some(motd);
225        self
226    }
227
228    /// Get a clone of the current MintInfo configured on the builder
229    /// This allows using config-derived settings to initialize persistent state
230    /// before any attempt to read from the database, which avoids first-run
231    /// failures when the DB is empty.
232    pub fn current_mint_info(&self) -> MintInfo {
233        self.mint_info.clone()
234    }
235
236    /// Set terms of service URL
237    pub fn with_tos_url(mut self, tos_url: String) -> Self {
238        self.mint_info.tos_url = Some(tos_url);
239        self
240    }
241
242    /// Set description
243    pub fn with_description(mut self, description: String) -> Self {
244        self.mint_info.description = Some(description);
245        self
246    }
247
248    /// Set long description
249    pub fn with_long_description(mut self, description: String) -> Self {
250        self.mint_info.description_long = Some(description);
251        self
252    }
253
254    /// Set version
255    pub fn with_version(mut self, version: MintVersion) -> Self {
256        self.mint_info.version = Some(version);
257        self
258    }
259
260    /// Set contact info
261    pub fn with_contact_info(mut self, contact_info: ContactInfo) -> Self {
262        let mut contacts = self.mint_info.contact.clone().unwrap_or_default();
263        contacts.push(contact_info);
264        self.mint_info.contact = Some(contacts);
265        self
266    }
267
268    /// Set pubkey
269    pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self {
270        self.mint_info.pubkey = Some(pubkey);
271
272        self
273    }
274
275    /// Support websockets
276    pub fn with_supported_websockets(mut self, supported_method: SupportedMethods) -> Self {
277        let mut supported_settings = self.mint_info.nuts.nut17.supported.clone();
278
279        if !supported_settings.contains(&supported_method) {
280            supported_settings.push(supported_method);
281
282            self.mint_info.nuts = self.mint_info.nuts.nut17(supported_settings);
283        }
284
285        self
286    }
287
288    /// Add support for NUT19
289    pub fn with_cache(mut self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
290        let nut19_settings = nut19::Settings {
291            ttl,
292            cached_endpoints,
293        };
294
295        self.mint_info.nuts.nut19 = nut19_settings;
296
297        self
298    }
299
300    /// Set custom derivation paths for mint units
301    pub fn with_custom_derivation_paths(
302        mut self,
303        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
304    ) -> Self {
305        self.custom_paths = custom_paths;
306        self
307    }
308
309    /// Set transaction limits for DoS protection
310    pub fn with_limits(mut self, max_inputs: usize, max_outputs: usize) -> Self {
311        self.max_inputs = max_inputs;
312        self.max_outputs = max_outputs;
313        self
314    }
315
316    /// Set batch minting settings (NUT-29)
317    ///
318    /// Configures the maximum number of quotes allowed in a single batch request
319    /// and optionally specifies which payment methods support batch minting.
320    ///
321    /// # Arguments
322    /// * `max_batch_size` - Maximum number of quotes in a batch request
323    /// * `methods` - Optional list of payment methods that support batch minting
324    pub fn with_batch_minting(
325        mut self,
326        max_batch_size: Option<u64>,
327        methods: Option<Vec<String>>,
328    ) -> Self {
329        self.max_batch_size = max_batch_size;
330        self.mint_info.nuts.nut29 = cdk_common::nut29::Settings::new(max_batch_size, methods);
331        self
332    }
333
334    /// Configure a unit with custom amounts and fee
335    ///
336    /// This is optional - if not called before [`add_payment_processor`](Self::add_payment_processor),
337    /// the unit will be auto-configured with default values (powers of 2 amounts, zero fee).
338    ///
339    /// # Arguments
340    /// * `unit` - The currency unit to configure
341    /// * `config` - The unit configuration (amounts and fee)
342    ///
343    /// # Example
344    /// ```rust,ignore
345    /// mint_builder.configure_unit(
346    ///     CurrencyUnit::Sat,
347    ///     UnitConfig {
348    ///         amounts: vec![1, 2, 4, 8, 16, 32],
349    ///         input_fee_ppk: 100,
350    ///     }
351    /// );
352    /// ```
353    pub fn configure_unit(&mut self, unit: CurrencyUnit, config: UnitConfig) -> Result<(), Error> {
354        // Validate amounts
355        if config.amounts.is_empty() {
356            return Err(Error::Custom("Amounts list cannot be empty".to_string()));
357        }
358
359        // Check for duplicates and ensure sorted
360        let mut sorted = config.amounts.clone();
361        sorted.sort_unstable();
362        sorted.dedup();
363        if sorted.len() != config.amounts.len() {
364            return Err(Error::Custom(
365                "Amounts list contains duplicates".to_string(),
366            ));
367        }
368        if sorted != config.amounts {
369            return Err(Error::Custom(
370                "Amounts must be sorted in ascending order".to_string(),
371            ));
372        }
373
374        // Check all amounts are positive
375        if config.amounts.contains(&0) {
376            return Err(Error::Custom("Amounts must be greater than 0".to_string()));
377        }
378
379        self.supported_units
380            .insert(unit, (config.input_fee_ppk, config.amounts));
381        Ok(())
382    }
383
384    /// Add a payment processor for the given unit and payment method
385    ///
386    /// If the unit has not been configured via [`configure_unit`](Self::configure_unit),
387    /// it will be auto-configured with default values (powers of 2 amounts, zero fee).
388    ///
389    /// # Arguments
390    /// * `unit` - The currency unit for this payment processor
391    /// * `method` - The payment method (e.g., bolt11, bolt12)
392    /// * `limits` - Mint and melt amount limits
393    /// * `payment_processor` - The payment processor implementation
394    pub async fn add_payment_processor(
395        &mut self,
396        unit: CurrencyUnit,
397        method: PaymentMethod,
398        limits: MintMeltLimits,
399        payment_processor: DynMintPayment,
400    ) -> Result<(), Error> {
401        let key = PaymentProcessorKey {
402            unit: unit.clone(),
403            method: method.clone(),
404        };
405        if self.payment_processors.contains_key(&key) {
406            return Err(Error::Custom(format!(
407                "Duplicate payment processor for unit {} and method {}",
408                unit, method
409            )));
410        }
411
412        let settings = payment_processor.get_settings().await?;
413
414        match method {
415            // Handle bolt11 methods
416            PaymentMethod::Known(KnownMethod::Bolt11) => {
417                if let Some(ref bolt11_settings) = settings.bolt11 {
418                    // Add MPP support if available
419                    if bolt11_settings.mpp {
420                        let mpp_settings = MppMethodSettings {
421                            method: method.clone(),
422                            unit: unit.clone(),
423                        };
424
425                        let mut mpp = self.mint_info.nuts.nut15.clone();
426                        mpp.methods.push(mpp_settings);
427                        self.mint_info.nuts.nut15 = mpp;
428                    }
429
430                    // Add to NUT04 (mint)
431                    let mint_method_settings = MintMethodSettings {
432                        method: method.clone(),
433                        unit: unit.clone(),
434                        min_amount: Some(limits.mint_min),
435                        max_amount: Some(limits.mint_max),
436                        options: Some(MintMethodOptions::Bolt11 {
437                            description: bolt11_settings.invoice_description,
438                        }),
439                    };
440                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
441                    self.mint_info.nuts.nut04.disabled = false;
442
443                    // Add to NUT05 (melt)
444                    let melt_method_settings = MeltMethodSettings {
445                        method: method.clone(),
446                        unit: unit.clone(),
447                        min_amount: Some(limits.melt_min),
448                        max_amount: Some(limits.melt_max),
449                        options: Some(MeltMethodOptions::Bolt11 {
450                            amountless: bolt11_settings.amountless,
451                        }),
452                    };
453                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
454                    self.mint_info.nuts.nut05.disabled = false;
455                }
456            }
457            // Handle bolt12 methods
458            PaymentMethod::Known(KnownMethod::Bolt12) => {
459                if settings.bolt12.is_some() {
460                    // Add to NUT04 (mint) - bolt12 doesn't have specific options yet
461                    let mint_method_settings = MintMethodSettings {
462                        method: method.clone(),
463                        unit: unit.clone(),
464                        min_amount: Some(limits.mint_min),
465                        max_amount: Some(limits.mint_max),
466                        options: None, // No bolt12-specific options in NUT04 yet
467                    };
468                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
469                    self.mint_info.nuts.nut04.disabled = false;
470
471                    // Add to NUT05 (melt) - bolt12 doesn't have specific options in MeltMethodOptions yet
472                    let melt_method_settings = MeltMethodSettings {
473                        method: method.clone(),
474                        unit: unit.clone(),
475                        min_amount: Some(limits.melt_min),
476                        max_amount: Some(limits.melt_max),
477                        options: None, // No bolt12-specific options in NUT05 yet
478                    };
479                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
480                    self.mint_info.nuts.nut05.disabled = false;
481                }
482            }
483            // Handle custom methods
484            PaymentMethod::Custom(_) => {
485                // Check if this custom method is supported by the payment processor
486                if settings.custom.contains_key(method.as_str()) {
487                    // Add to NUT04 (mint)
488                    let mint_method_settings = MintMethodSettings {
489                        method: method.clone(),
490                        unit: unit.clone(),
491                        min_amount: Some(limits.mint_min),
492                        max_amount: Some(limits.mint_max),
493                        options: Some(MintMethodOptions::Custom {}),
494                    };
495                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
496                    self.mint_info.nuts.nut04.disabled = false;
497
498                    // Add to NUT05 (melt)
499                    let melt_method_settings = MeltMethodSettings {
500                        method: method.clone(),
501                        unit: unit.clone(),
502                        min_amount: Some(limits.melt_min),
503                        max_amount: Some(limits.melt_max),
504                        options: None, // No custom-specific options in NUT05 yet
505                    };
506                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
507                    self.mint_info.nuts.nut05.disabled = false;
508                }
509            }
510            // Handle onchain methods
511            PaymentMethod::Known(KnownMethod::Onchain) => {
512                if let Some(onchain_settings) = settings.onchain {
513                    let mint_min = Amount::from(
514                        limits
515                            .mint_min
516                            .to_u64()
517                            .max(onchain_settings.min_receive_amount_sat),
518                    );
519                    let melt_min = Amount::from(
520                        limits
521                            .melt_min
522                            .to_u64()
523                            .max(onchain_settings.min_send_amount_sat),
524                    );
525
526                    // Add to NUT04 (mint)
527                    let mint_method_settings = MintMethodSettings {
528                        method: method.clone(),
529                        unit: unit.clone(),
530                        min_amount: Some(mint_min),
531                        max_amount: Some(limits.mint_max),
532                        options: Some(MintMethodOptions::Onchain {
533                            confirmations: onchain_settings.confirmations,
534                        }),
535                    };
536                    self.mint_info.nuts.nut04.methods.push(mint_method_settings);
537                    self.mint_info.nuts.nut04.disabled = false;
538
539                    // Add to NUT05 (melt)
540                    let melt_method_settings = MeltMethodSettings {
541                        method: method.clone(),
542                        unit: unit.clone(),
543                        min_amount: Some(melt_min),
544                        max_amount: Some(limits.melt_max),
545                        options: None,
546                    };
547                    self.mint_info.nuts.nut05.methods.push(melt_method_settings);
548                    self.mint_info.nuts.nut05.disabled = false;
549                }
550            }
551        }
552
553        // Check that the unit has been pre-configured
554        if !self.supported_units.contains_key(&key.unit) {
555            self.configure_unit(key.unit.clone(), Default::default())?;
556        }
557
558        self.payment_processors.insert(key, payment_processor);
559        Ok(())
560    }
561    /// Sets the input fee ppk for a given unit
562    ///
563    /// The unit **MUST** already have been added with a ln backend
564    pub fn set_unit_fee(&mut self, unit: &CurrencyUnit, input_fee_ppk: u64) -> Result<(), Error> {
565        let (input_fee, _) = self
566            .supported_units
567            .get_mut(unit)
568            .ok_or(Error::UnsupportedUnit)?;
569
570        *input_fee = input_fee_ppk;
571
572        Ok(())
573    }
574
575    /// Build the mint with the provided signatory
576    pub async fn build_with_signatory(
577        #[allow(unused_mut)] mut self,
578        signatory: Arc<dyn Signatory + Send + Sync>,
579    ) -> Result<Mint, Error> {
580        // Check active keysets and rotate if necessary
581        let active_keysets = signatory.keysets().await?;
582
583        // Ensure Auth keyset is created when auth is enabled
584        if self.auth_localstore.is_some() {
585            self.supported_units
586                .entry(CurrencyUnit::Auth)
587                .or_insert((0, vec![1]));
588        }
589
590        for (unit, (fee, amounts)) in &self.supported_units {
591            // Check if we have an active keyset for this unit
592            let keyset = active_keysets
593                .keysets
594                .iter()
595                .find(|k| k.active && k.unit == *unit);
596
597            let mut rotate = false;
598
599            if let Some(keyset) = keyset {
600                if keyset.is_expired() {
601                    tracing::warn!("Active keyset for unit {} has expired; not rotating", unit);
602                    continue;
603                }
604                // Check if fee matches
605                if keyset.input_fee_ppk != *fee {
606                    tracing::info!(
607                        "Rotating keyset for unit {} due to fee mismatch (current: {}, expected: {})",
608                        unit,
609                        keyset.input_fee_ppk,
610                        fee
611                    );
612                    rotate = true;
613                }
614
615                // Check if amounts match
616                if keyset.amounts != *amounts {
617                    tracing::info!("Rotating keyset for unit {} due to amounts mismatch", unit);
618                    rotate = true;
619                }
620
621                // Check if version matches explicit preference
622                if let Some(want_v2) = self.use_keyset_v2 {
623                    let is_v2 =
624                        keyset.id.get_version() == cdk_common::nut02::KeySetVersion::Version01;
625                    if want_v2 && !is_v2 {
626                        tracing::info!("Rotating keyset for unit {} due to explicit V2 preference (current is V1)", unit);
627                        rotate = true;
628                    } else if !want_v2 && is_v2 {
629                        tracing::info!("Rotating keyset for unit {} due to explicit V1 preference (current is V2)", unit);
630                        rotate = true;
631                    }
632                }
633            } else {
634                // No active keyset for this unit
635                tracing::info!("Rotating keyset for unit {} (no active keyset found)", unit);
636                rotate = true;
637            }
638
639            if rotate {
640                signatory
641                    .rotate_keyset(RotateKeyArguments {
642                        unit: unit.clone(),
643                        amounts: amounts.clone(),
644                        input_fee_ppk: *fee,
645                        keyset_id_type: if self.use_keyset_v2.unwrap_or(true) {
646                            cdk_common::nut02::KeySetVersion::Version01
647                        } else {
648                            cdk_common::nut02::KeySetVersion::Version00
649                        },
650                        final_expiry: None,
651                    })
652                    .await?;
653            }
654        }
655
656        // Execute configured keyset rotations (e.g. for test keysets)
657        for rotation in &self.keyset_rotations {
658            signatory
659                .rotate_keyset(RotateKeyArguments {
660                    unit: rotation.unit.clone(),
661                    amounts: rotation.amounts.clone(),
662                    input_fee_ppk: rotation.input_fee_ppk,
663                    keyset_id_type: if rotation.use_keyset_v2 {
664                        cdk_common::nut02::KeySetVersion::Version01
665                    } else {
666                        cdk_common::nut02::KeySetVersion::Version00
667                    },
668                    final_expiry: rotation.final_expiry,
669                })
670                .await?;
671        }
672
673        if self.blind_auth_configured
674            && self.mint_info.nuts.nut22.is_some()
675            && self.auth_localstore.is_none()
676        {
677            return Err(Error::Custom(
678                "Blind auth requires an auth localstore; call MintBuilder::with_auth before MintBuilder::with_blind_auth".to_string(),
679            ));
680        }
681
682        if let Some(auth_localstore) = self.auth_localstore {
683            let mut protected_endpoints = HashMap::new();
684            for endpoint in self.clear_auth_endpoints {
685                protected_endpoints.insert(endpoint, AuthRequired::Clear);
686            }
687            for endpoint in self.blind_auth_endpoints {
688                protected_endpoints.insert(endpoint, AuthRequired::Blind);
689            }
690
691            if !protected_endpoints.is_empty() {
692                let mut tx = auth_localstore.begin_transaction().await?;
693                tx.add_protected_endpoints(protected_endpoints).await?;
694                tx.commit().await?;
695            }
696
697            return Mint::new_with_auth(
698                self.mint_info,
699                signatory,
700                self.localstore,
701                auth_localstore,
702                self.payment_processors,
703                self.max_inputs,
704                self.max_outputs,
705            )
706            .await;
707        }
708        Mint::new(
709            self.mint_info,
710            signatory,
711            self.localstore,
712            self.payment_processors,
713            self.max_inputs,
714            self.max_outputs,
715        )
716        .await
717    }
718
719    /// Build the mint with the provided keystore and seed
720    pub async fn build_with_seed(
721        self,
722        keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
723        seed: &[u8],
724    ) -> Result<Mint, Error> {
725        let in_memory_signatory = cdk_signatory::db_signatory::DbSignatory::new(
726            keystore,
727            seed,
728            self.supported_units.clone(),
729            self.custom_paths.clone(),
730        )
731        .await?;
732
733        let signatory = Arc::new(cdk_signatory::embedded::Service::new(Arc::new(
734            in_memory_signatory,
735        )));
736
737        self.build_with_signatory(signatory).await
738    }
739}
740
741/// Mint and Melt Limits
742#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
743pub struct MintMeltLimits {
744    /// Min mint amount
745    pub mint_min: Amount,
746    /// Max mint amount
747    pub mint_max: Amount,
748    /// Min melt amount
749    pub melt_min: Amount,
750    /// Max melt amount
751    pub melt_max: Amount,
752}
753
754impl MintMeltLimits {
755    /// Create new [`MintMeltLimits`]. The `min` and `max` limits apply to both minting and melting.
756    pub fn new(min: u64, max: u64) -> Self {
757        Self {
758            mint_min: min.into(),
759            mint_max: max.into(),
760            melt_min: min.into(),
761            melt_max: max.into(),
762        }
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use std::collections::HashMap;
769    use std::pin::Pin;
770    use std::sync::Arc;
771
772    use async_trait::async_trait;
773    use bip39::Mnemonic;
774    use cdk_common::nut21::{Method, RoutePath};
775    use cdk_common::payment::{
776        Bolt11Settings, Bolt12Settings, CreateIncomingPaymentResponse, Event,
777        IncomingPaymentOptions, MakePaymentResponse, OnchainSettings, OutgoingPaymentOptions,
778        PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
779    };
780    use cdk_sqlite::mint::{memory, MintSqliteAuthDatabase, MintSqliteDatabase};
781    use futures::Stream;
782    use KnownMethod;
783
784    use super::*;
785
786    // Mock payment processor for testing
787    struct MockPaymentProcessor {
788        settings: SettingsResponse,
789    }
790
791    #[async_trait]
792    impl cdk_common::payment::MintPayment for MockPaymentProcessor {
793        type Err = cdk_common::payment::Error;
794
795        async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
796            Ok(self.settings.clone())
797        }
798
799        async fn create_incoming_payment_request(
800            &self,
801            _options: IncomingPaymentOptions,
802        ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
803            unimplemented!()
804        }
805
806        async fn get_payment_quote(
807            &self,
808            _unit: &CurrencyUnit,
809            _options: OutgoingPaymentOptions,
810        ) -> Result<PaymentQuoteResponse, Self::Err> {
811            unimplemented!()
812        }
813
814        async fn make_payment(
815            &self,
816            _unit: &CurrencyUnit,
817            _options: OutgoingPaymentOptions,
818        ) -> Result<MakePaymentResponse, Self::Err> {
819            unimplemented!()
820        }
821
822        async fn wait_payment_event(
823            &self,
824        ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
825            unimplemented!()
826        }
827
828        fn is_payment_event_stream_active(&self) -> bool {
829            false
830        }
831
832        fn cancel_payment_event_stream(&self) {}
833
834        async fn check_incoming_payment_status(
835            &self,
836            _payment_identifier: &PaymentIdentifier,
837        ) -> Result<Vec<cdk_common::payment::WaitPaymentResponse>, Self::Err> {
838            unimplemented!()
839        }
840
841        async fn check_outgoing_payment(
842            &self,
843            _payment_identifier: &PaymentIdentifier,
844        ) -> Result<MakePaymentResponse, Self::Err> {
845            unimplemented!()
846        }
847    }
848
849    async fn builder_with_bolt11_processor() -> (MintBuilder, Arc<MintSqliteDatabase>) {
850        let localstore = Arc::new(memory::empty().await.expect("mint db"));
851        let mut builder = MintBuilder::new(localstore.clone());
852
853        let settings = SettingsResponse {
854            unit: "sat".to_string(),
855            bolt11: Some(Bolt11Settings {
856                mpp: false,
857                amountless: false,
858                invoice_description: false,
859            }),
860            bolt12: None,
861            onchain: None,
862            custom: HashMap::new(),
863        };
864
865        builder
866            .add_payment_processor(
867                CurrencyUnit::Sat,
868                PaymentMethod::Known(KnownMethod::Bolt11),
869                MintMeltLimits::new(1, 10_000),
870                Arc::new(MockPaymentProcessor { settings }),
871            )
872            .await
873            .expect("payment processor");
874
875        (builder, localstore)
876    }
877
878    fn seed() -> Vec<u8> {
879        Mnemonic::generate(12)
880            .expect("mnemonic")
881            .to_seed_normalized("")
882            .to_vec()
883    }
884
885    #[tokio::test]
886    async fn test_mint_builder_default_nuts_support() {
887        let localstore = Arc::new(memory::empty().await.unwrap());
888        let builder = MintBuilder::new(localstore);
889        let mint_info = builder.current_mint_info();
890
891        assert!(
892            mint_info.nuts.nut07.supported,
893            "NUT-07 should be supported by default"
894        );
895        assert!(
896            mint_info.nuts.nut08.supported,
897            "NUT-08 should be supported by default"
898        );
899        assert!(
900            mint_info.nuts.nut09.supported,
901            "NUT-09 should be supported by default"
902        );
903        assert!(
904            mint_info.nuts.nut10.supported,
905            "NUT-10 should be supported by default"
906        );
907        assert!(
908            mint_info.nuts.nut11.supported,
909            "NUT-11 should be supported by default"
910        );
911        assert!(
912            mint_info.nuts.nut12.supported,
913            "NUT-12 should be supported by default"
914        );
915        assert!(
916            mint_info.nuts.nut14.supported,
917            "NUT-14 (HTLC) should be supported by default"
918        );
919        assert!(
920            mint_info.nuts.nut20.supported,
921            "NUT-20 should be supported by default"
922        );
923        assert!(
924            mint_info.nuts.nut29.is_empty(),
925            "NUT-29 should have empty settings by default"
926        );
927    }
928
929    #[tokio::test]
930    async fn test_mint_builder_batch_minting_settings() {
931        let localstore = Arc::new(memory::empty().await.unwrap());
932        let builder = MintBuilder::new(localstore).with_batch_minting(
933            Some(100),
934            Some(vec!["bolt11".to_string(), "bolt12".to_string()]),
935        );
936        let mint_info = builder.current_mint_info();
937
938        assert_eq!(
939            mint_info.nuts.nut29.max_batch_size,
940            Some(100),
941            "NUT-29 max_batch_size should be set"
942        );
943        assert_eq!(
944            mint_info.nuts.nut29.methods,
945            Some(vec!["bolt11".to_string(), "bolt12".to_string()]),
946            "NUT-29 methods should be set"
947        );
948    }
949
950    #[tokio::test]
951    async fn test_with_auth_protected_endpoints_are_enforced_and_advertised() {
952        let (builder, localstore) = builder_with_bolt11_processor().await;
953        let auth_db = Arc::new(
954            MintSqliteAuthDatabase::new(":memory:")
955                .await
956                .expect("auth db"),
957        );
958        let swap_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
959        let seed = seed();
960
961        let mint = builder
962            .with_auth(
963                auth_db,
964                "https://example.com/.well-known/openid-configuration".to_string(),
965                "test-client".to_string(),
966                vec![swap_endpoint.clone()],
967            )
968            .build_with_seed(localstore, &seed)
969            .await
970            .expect("mint");
971
972        assert_eq!(
973            mint.is_protected(&swap_endpoint)
974                .await
975                .expect("is protected"),
976            Some(AuthRequired::Clear)
977        );
978        assert_eq!(
979            mint.mint_info()
980                .await
981                .expect("mint info")
982                .protected_endpoints()
983                .get(&swap_endpoint),
984            Some(&AuthRequired::Clear)
985        );
986    }
987
988    #[tokio::test]
989    async fn test_with_blind_auth_protected_endpoints_are_enforced_and_advertised() {
990        let (builder, localstore) = builder_with_bolt11_processor().await;
991        let auth_db = Arc::new(
992            MintSqliteAuthDatabase::new(":memory:")
993                .await
994                .expect("auth db"),
995        );
996        let swap_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
997        let seed = seed();
998
999        let mint = builder
1000            .with_auth(
1001                auth_db,
1002                "https://example.com/.well-known/openid-configuration".to_string(),
1003                "test-client".to_string(),
1004                vec![],
1005            )
1006            .with_blind_auth(50, vec![swap_endpoint.clone()])
1007            .build_with_seed(localstore, &seed)
1008            .await
1009            .expect("mint");
1010
1011        assert_eq!(
1012            mint.is_protected(&swap_endpoint)
1013                .await
1014                .expect("is protected"),
1015            Some(AuthRequired::Blind)
1016        );
1017        assert_eq!(
1018            mint.mint_info()
1019                .await
1020                .expect("mint info")
1021                .protected_endpoints()
1022                .get(&swap_endpoint),
1023            Some(&AuthRequired::Blind)
1024        );
1025    }
1026
1027    #[tokio::test]
1028    async fn test_with_blind_auth_without_auth_localstore_errors() {
1029        let (builder, localstore) = builder_with_bolt11_processor().await;
1030        let swap_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
1031        let seed = seed();
1032
1033        let err = builder
1034            .with_blind_auth(50, vec![swap_endpoint])
1035            .build_with_seed(localstore, &seed)
1036            .await
1037            .expect_err("blind auth requires auth localstore");
1038
1039        assert!(matches!(
1040            err,
1041            Error::Custom(message) if message.contains("Blind auth requires an auth localstore")
1042        ));
1043    }
1044
1045    #[tokio::test]
1046    async fn test_add_payment_processor_bolt11() {
1047        let localstore = Arc::new(memory::empty().await.unwrap());
1048        let mut builder = MintBuilder::new(localstore);
1049
1050        // Configure the unit first
1051        builder
1052            .configure_unit(
1053                CurrencyUnit::Sat,
1054                UnitConfig {
1055                    amounts: vec![1, 2, 4, 8, 16, 32],
1056                    input_fee_ppk: 0,
1057                },
1058            )
1059            .unwrap();
1060
1061        let bolt11_settings = Bolt11Settings {
1062            mpp: true,
1063            amountless: true,
1064            invoice_description: true,
1065        };
1066
1067        let settings = SettingsResponse {
1068            unit: "sat".to_string(),
1069            bolt11: Some(bolt11_settings),
1070            bolt12: None,
1071            onchain: None,
1072            custom: HashMap::new(),
1073        };
1074
1075        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1076        let unit = CurrencyUnit::Sat;
1077        let method = PaymentMethod::Known(KnownMethod::Bolt11);
1078        let limits = MintMeltLimits::new(100, 10000);
1079
1080        builder
1081            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
1082            .await
1083            .unwrap();
1084
1085        let mint_info = builder.current_mint_info();
1086
1087        // Check NUT04 (mint) settings
1088        assert!(!mint_info.nuts.nut04.disabled);
1089        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
1090        let mint_method = &mint_info.nuts.nut04.methods[0];
1091        assert_eq!(mint_method.method, method);
1092        assert_eq!(mint_method.unit, unit);
1093        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
1094        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
1095        assert!(matches!(
1096            mint_method.options,
1097            Some(MintMethodOptions::Bolt11 { description: true })
1098        ));
1099
1100        // Check NUT05 (melt) settings
1101        assert!(!mint_info.nuts.nut05.disabled);
1102        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
1103        let melt_method = &mint_info.nuts.nut05.methods[0];
1104        assert_eq!(melt_method.method, method);
1105        assert_eq!(melt_method.unit, unit);
1106        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
1107        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
1108        assert!(matches!(
1109            melt_method.options,
1110            Some(MeltMethodOptions::Bolt11 { amountless: true })
1111        ));
1112
1113        // Check NUT15 (MPP) settings
1114        assert_eq!(mint_info.nuts.nut15.methods.len(), 1);
1115        let mpp_method = &mint_info.nuts.nut15.methods[0];
1116        assert_eq!(mpp_method.method, method);
1117        assert_eq!(mpp_method.unit, unit);
1118    }
1119
1120    #[tokio::test]
1121    async fn test_add_payment_processor_rejects_duplicate_unit_method() {
1122        let localstore = Arc::new(memory::empty().await.unwrap());
1123        let mut builder = MintBuilder::new(localstore);
1124
1125        let settings = SettingsResponse {
1126            unit: "sat".to_string(),
1127            bolt11: Some(Bolt11Settings {
1128                mpp: false,
1129                amountless: false,
1130                invoice_description: false,
1131            }),
1132            bolt12: None,
1133            onchain: None,
1134            custom: HashMap::new(),
1135        };
1136        let unit = CurrencyUnit::Sat;
1137        let method = PaymentMethod::Known(KnownMethod::Bolt11);
1138        let limits = MintMeltLimits::new(100, 10000);
1139
1140        builder
1141            .add_payment_processor(
1142                unit.clone(),
1143                method.clone(),
1144                limits,
1145                Arc::new(MockPaymentProcessor {
1146                    settings: settings.clone(),
1147                }),
1148            )
1149            .await
1150            .unwrap();
1151
1152        let err = builder
1153            .add_payment_processor(
1154                unit,
1155                method,
1156                limits,
1157                Arc::new(MockPaymentProcessor { settings }),
1158            )
1159            .await
1160            .expect_err("duplicate unit/method pair should be rejected");
1161
1162        assert!(matches!(
1163            err,
1164            Error::Custom(message) if message.contains("Duplicate payment processor")
1165        ));
1166    }
1167
1168    #[tokio::test]
1169    async fn test_add_payment_processor_bolt11_without_mpp() {
1170        let localstore = Arc::new(memory::empty().await.unwrap());
1171        let mut builder = MintBuilder::new(localstore);
1172
1173        // Configure the unit first
1174        builder
1175            .configure_unit(
1176                CurrencyUnit::Sat,
1177                UnitConfig {
1178                    amounts: vec![1, 2, 4, 8, 16, 32],
1179                    input_fee_ppk: 0,
1180                },
1181            )
1182            .unwrap();
1183
1184        let bolt11_settings = Bolt11Settings {
1185            mpp: false, // MPP disabled
1186            amountless: false,
1187            invoice_description: false,
1188        };
1189
1190        let settings = SettingsResponse {
1191            unit: "sat".to_string(),
1192            bolt11: Some(bolt11_settings),
1193            bolt12: None,
1194            onchain: None,
1195            custom: HashMap::new(),
1196        };
1197
1198        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1199        let unit = CurrencyUnit::Sat;
1200        let method = PaymentMethod::Known(KnownMethod::Bolt11);
1201        let limits = MintMeltLimits::new(100, 10000);
1202
1203        builder
1204            .add_payment_processor(unit, method, limits, payment_processor)
1205            .await
1206            .unwrap();
1207
1208        let mint_info = builder.current_mint_info();
1209
1210        // NUT15 should be empty when MPP is disabled
1211        assert_eq!(mint_info.nuts.nut15.methods.len(), 0);
1212
1213        // But NUT04 and NUT05 should still be populated
1214        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
1215        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
1216    }
1217
1218    #[tokio::test]
1219    async fn test_add_payment_processor_bolt12() {
1220        let localstore = Arc::new(memory::empty().await.unwrap());
1221        let mut builder = MintBuilder::new(localstore);
1222
1223        // Configure the unit first
1224        builder
1225            .configure_unit(
1226                CurrencyUnit::Sat,
1227                UnitConfig {
1228                    amounts: vec![1, 2, 4, 8, 16, 32],
1229                    input_fee_ppk: 0,
1230                },
1231            )
1232            .unwrap();
1233
1234        let bolt12_settings = Bolt12Settings { amountless: true };
1235
1236        let settings = SettingsResponse {
1237            unit: "sat".to_string(),
1238            bolt11: None,
1239            bolt12: Some(bolt12_settings),
1240            onchain: None,
1241            custom: HashMap::new(),
1242        };
1243
1244        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1245        let unit = CurrencyUnit::Sat;
1246        let method = PaymentMethod::Known(KnownMethod::Bolt12);
1247        let limits = MintMeltLimits::new(100, 10000);
1248
1249        builder
1250            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
1251            .await
1252            .unwrap();
1253
1254        let mint_info = builder.current_mint_info();
1255
1256        // Check NUT04 (mint) settings
1257        assert!(!mint_info.nuts.nut04.disabled);
1258        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
1259        let mint_method = &mint_info.nuts.nut04.methods[0];
1260        assert_eq!(mint_method.method, method);
1261        assert_eq!(mint_method.unit, unit);
1262        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
1263        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
1264        assert!(mint_method.options.is_none());
1265
1266        // Check NUT05 (melt) settings
1267        assert!(!mint_info.nuts.nut05.disabled);
1268        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
1269        let melt_method = &mint_info.nuts.nut05.methods[0];
1270        assert_eq!(melt_method.method, method);
1271        assert_eq!(melt_method.unit, unit);
1272        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
1273        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
1274        assert!(melt_method.options.is_none());
1275    }
1276
1277    #[tokio::test]
1278    async fn test_add_payment_processor_custom() {
1279        let localstore = Arc::new(memory::empty().await.unwrap());
1280        let mut builder = MintBuilder::new(localstore);
1281
1282        // Configure the unit first
1283        builder
1284            .configure_unit(
1285                CurrencyUnit::Usd,
1286                UnitConfig {
1287                    amounts: vec![1, 2, 4, 8, 16, 32],
1288                    input_fee_ppk: 0,
1289                },
1290            )
1291            .unwrap();
1292
1293        let mut custom_methods = HashMap::new();
1294        custom_methods.insert("paypal".to_string(), "{}".to_string());
1295
1296        let settings = SettingsResponse {
1297            unit: "usd".to_string(),
1298            bolt11: None,
1299            bolt12: None,
1300            onchain: None,
1301            custom: custom_methods,
1302        };
1303
1304        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1305        let unit = CurrencyUnit::Usd;
1306        let method = PaymentMethod::Custom("paypal".to_string());
1307        let limits = MintMeltLimits::new(100, 10000);
1308
1309        builder
1310            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
1311            .await
1312            .unwrap();
1313
1314        let mint_info = builder.current_mint_info();
1315
1316        // Check NUT04 (mint) settings
1317        assert!(!mint_info.nuts.nut04.disabled);
1318        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
1319        let mint_method = &mint_info.nuts.nut04.methods[0];
1320        assert_eq!(mint_method.method, method);
1321        assert_eq!(mint_method.unit, unit);
1322        assert_eq!(mint_method.min_amount, Some(limits.mint_min));
1323        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
1324        assert!(matches!(
1325            mint_method.options,
1326            Some(MintMethodOptions::Custom {})
1327        ));
1328
1329        // Check NUT05 (melt) settings
1330        assert!(!mint_info.nuts.nut05.disabled);
1331        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
1332        let melt_method = &mint_info.nuts.nut05.methods[0];
1333        assert_eq!(melt_method.method, method);
1334        assert_eq!(melt_method.unit, unit);
1335        assert_eq!(melt_method.min_amount, Some(limits.melt_min));
1336        assert_eq!(melt_method.max_amount, Some(limits.melt_max));
1337        assert!(melt_method.options.is_none());
1338    }
1339
1340    #[tokio::test]
1341    async fn test_add_payment_processor_custom_not_supported() {
1342        let localstore = Arc::new(memory::empty().await.unwrap());
1343        let mut builder = MintBuilder::new(localstore);
1344
1345        // Configure the unit first
1346        builder
1347            .configure_unit(
1348                CurrencyUnit::Usd,
1349                UnitConfig {
1350                    amounts: vec![1, 2, 4, 8, 16, 32],
1351                    input_fee_ppk: 0,
1352                },
1353            )
1354            .unwrap();
1355
1356        // Settings with no custom methods
1357        let settings = SettingsResponse {
1358            unit: "usd".to_string(),
1359            bolt11: None,
1360            bolt12: None,
1361            onchain: None,
1362            custom: HashMap::new(), // Empty - no custom methods supported
1363        };
1364
1365        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1366        let unit = CurrencyUnit::Usd;
1367        let method = PaymentMethod::Custom("paypal".to_string());
1368        let limits = MintMeltLimits::new(1, 1000);
1369
1370        builder
1371            .add_payment_processor(unit, method, limits, payment_processor)
1372            .await
1373            .unwrap();
1374
1375        let mint_info = builder.current_mint_info();
1376
1377        // NUT04 and NUT05 should remain empty since the custom method is not in settings
1378        assert_eq!(mint_info.nuts.nut04.methods.len(), 0);
1379        assert_eq!(mint_info.nuts.nut05.methods.len(), 0);
1380    }
1381
1382    #[tokio::test]
1383    async fn test_add_payment_processor_onchain_uses_backend_min_receive_amount() {
1384        let localstore = Arc::new(memory::empty().await.unwrap());
1385        let mut builder = MintBuilder::new(localstore);
1386
1387        builder
1388            .configure_unit(
1389                CurrencyUnit::Sat,
1390                UnitConfig {
1391                    amounts: vec![1, 2, 4, 8, 16, 32],
1392                    input_fee_ppk: 0,
1393                },
1394            )
1395            .unwrap();
1396
1397        let settings = SettingsResponse {
1398            unit: "sat".to_string(),
1399            bolt11: None,
1400            bolt12: None,
1401            onchain: Some(OnchainSettings {
1402                confirmations: 6,
1403                min_receive_amount_sat: 1000,
1404                min_send_amount_sat: 546,
1405            }),
1406            custom: HashMap::new(),
1407        };
1408
1409        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1410        let unit = CurrencyUnit::Sat;
1411        let method = PaymentMethod::Known(KnownMethod::Onchain);
1412        let limits = MintMeltLimits::new(100, 10000);
1413
1414        builder
1415            .add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
1416            .await
1417            .unwrap();
1418
1419        let mint_info = builder.current_mint_info();
1420
1421        assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
1422        let mint_method = &mint_info.nuts.nut04.methods[0];
1423        assert_eq!(mint_method.method, method);
1424        assert_eq!(mint_method.unit, unit);
1425        assert_eq!(mint_method.min_amount, Some(Amount::from(1000)));
1426        assert_eq!(mint_method.max_amount, Some(limits.mint_max));
1427        assert!(matches!(
1428            mint_method.options,
1429            Some(MintMethodOptions::Onchain { confirmations: 6 })
1430        ));
1431
1432        assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
1433        let melt_method = &mint_info.nuts.nut05.methods[0];
1434        assert_eq!(melt_method.min_amount, Some(Amount::from(546)));
1435    }
1436
1437    #[tokio::test]
1438    async fn test_add_payment_processor_onchain_keeps_higher_configured_mint_min() {
1439        let localstore = Arc::new(memory::empty().await.unwrap());
1440        let mut builder = MintBuilder::new(localstore);
1441
1442        let settings = SettingsResponse {
1443            unit: "sat".to_string(),
1444            bolt11: None,
1445            bolt12: None,
1446            onchain: Some(OnchainSettings {
1447                confirmations: 1,
1448                min_receive_amount_sat: 1000,
1449                min_send_amount_sat: 546,
1450            }),
1451            custom: HashMap::new(),
1452        };
1453
1454        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1455        let limits = MintMeltLimits::new(2000, 10000);
1456
1457        builder
1458            .add_payment_processor(
1459                CurrencyUnit::Sat,
1460                PaymentMethod::Known(KnownMethod::Onchain),
1461                limits,
1462                payment_processor,
1463            )
1464            .await
1465            .unwrap();
1466
1467        let mint_info = builder.current_mint_info();
1468        let mint_method = &mint_info.nuts.nut04.methods[0];
1469
1470        assert_eq!(mint_method.min_amount, Some(Amount::from(2000)));
1471    }
1472
1473    #[tokio::test]
1474    async fn test_add_payment_processor_onchain_keeps_higher_configured_melt_min() {
1475        let localstore = Arc::new(memory::empty().await.unwrap());
1476        let mut builder = MintBuilder::new(localstore);
1477
1478        let settings = SettingsResponse {
1479            unit: "sat".to_string(),
1480            bolt11: None,
1481            bolt12: None,
1482            onchain: Some(OnchainSettings {
1483                confirmations: 1,
1484                min_receive_amount_sat: 1000,
1485                min_send_amount_sat: 546,
1486            }),
1487            custom: HashMap::new(),
1488        };
1489
1490        let payment_processor = Arc::new(MockPaymentProcessor { settings });
1491        let limits = MintMeltLimits {
1492            mint_min: Amount::from(1),
1493            mint_max: Amount::from(10_000),
1494            melt_min: Amount::from(2_000),
1495            melt_max: Amount::from(10_000),
1496        };
1497
1498        builder
1499            .add_payment_processor(
1500                CurrencyUnit::Sat,
1501                PaymentMethod::Known(KnownMethod::Onchain),
1502                limits,
1503                payment_processor,
1504            )
1505            .await
1506            .unwrap();
1507
1508        let mint_info = builder.current_mint_info();
1509        let melt_method = &mint_info.nuts.nut05.methods[0];
1510
1511        assert_eq!(melt_method.min_amount, Some(Amount::from(2_000)));
1512    }
1513
1514    #[tokio::test]
1515    async fn test_add_multiple_payment_processors() {
1516        let localstore = Arc::new(memory::empty().await.unwrap());
1517        let mut builder = MintBuilder::new(localstore);
1518
1519        // Configure the unit first
1520        builder
1521            .configure_unit(
1522                CurrencyUnit::Sat,
1523                UnitConfig {
1524                    amounts: vec![1, 2, 4, 8, 16, 32],
1525                    input_fee_ppk: 0,
1526                },
1527            )
1528            .unwrap();
1529
1530        // Add Bolt11
1531        let bolt11_settings = Bolt11Settings {
1532            mpp: false,
1533            amountless: true,
1534            invoice_description: false,
1535        };
1536        let settings1 = SettingsResponse {
1537            unit: "sat".to_string(),
1538            bolt11: Some(bolt11_settings),
1539            bolt12: None,
1540            onchain: None,
1541            custom: HashMap::new(),
1542        };
1543        let processor1 = Arc::new(MockPaymentProcessor {
1544            settings: settings1,
1545        });
1546        builder
1547            .add_payment_processor(
1548                CurrencyUnit::Sat,
1549                PaymentMethod::Known(KnownMethod::Bolt11),
1550                MintMeltLimits::new(100, 10000),
1551                processor1,
1552            )
1553            .await
1554            .unwrap();
1555
1556        // Add Bolt12
1557        let bolt12_settings = Bolt12Settings { amountless: false };
1558        let settings2 = SettingsResponse {
1559            unit: "sat".to_string(),
1560            bolt11: None,
1561            bolt12: Some(bolt12_settings),
1562            onchain: None,
1563            custom: HashMap::new(),
1564        };
1565        let processor2 = Arc::new(MockPaymentProcessor {
1566            settings: settings2,
1567        });
1568        builder
1569            .add_payment_processor(
1570                CurrencyUnit::Sat,
1571                PaymentMethod::Known(KnownMethod::Bolt12),
1572                MintMeltLimits::new(200, 20000),
1573                processor2,
1574            )
1575            .await
1576            .unwrap();
1577
1578        let mint_info = builder.current_mint_info();
1579
1580        // Should have both methods in NUT04 and NUT05
1581        assert_eq!(mint_info.nuts.nut04.methods.len(), 2);
1582        assert_eq!(mint_info.nuts.nut05.methods.len(), 2);
1583    }
1584}