1use 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#[derive(Debug, Clone)]
30pub struct UnitConfig {
31 pub amounts: Vec<u64>,
33 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#[derive(Debug, Clone)]
49pub struct KeysetRotation {
50 pub unit: CurrencyUnit,
52 pub amounts: Vec<u64>,
54 pub input_fee_ppk: u64,
56 pub use_keyset_v2: bool,
58 pub final_expiry: Option<u64>,
60}
61
62pub 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 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 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 pub fn with_keyset_rotation(mut self, rotation: KeysetRotation) -> Self {
133 self.keyset_rotations.push(rotation);
134 self
135 }
136
137 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 pub async fn init_from_db_if_present(&mut self) -> Result<(), cdk_database::Error> {
158 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 tracing::warn!("Failed to parse existing mint_info from DB; using builder state");
174 }
175 }
176
177 Ok(())
178 }
179
180 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 pub fn with_mint_info(mut self, mint_info: MintInfo) -> Self {
200 self.mint_info = mint_info;
201 self
202 }
203
204 pub fn with_name(mut self, name: String) -> Self {
206 self.mint_info.name = Some(name);
207 self
208 }
209
210 pub fn with_urls(mut self, urls: Vec<String>) -> Self {
212 self.mint_info.urls = Some(urls);
213 self
214 }
215
216 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 pub fn with_motd(mut self, motd: String) -> Self {
224 self.mint_info.motd = Some(motd);
225 self
226 }
227
228 pub fn current_mint_info(&self) -> MintInfo {
233 self.mint_info.clone()
234 }
235
236 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 pub fn with_description(mut self, description: String) -> Self {
244 self.mint_info.description = Some(description);
245 self
246 }
247
248 pub fn with_long_description(mut self, description: String) -> Self {
250 self.mint_info.description_long = Some(description);
251 self
252 }
253
254 pub fn with_version(mut self, version: MintVersion) -> Self {
256 self.mint_info.version = Some(version);
257 self
258 }
259
260 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 pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self {
270 self.mint_info.pubkey = Some(pubkey);
271
272 self
273 }
274
275 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 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 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 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 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 pub fn configure_unit(&mut self, unit: CurrencyUnit, config: UnitConfig) -> Result<(), Error> {
354 if config.amounts.is_empty() {
356 return Err(Error::Custom("Amounts list cannot be empty".to_string()));
357 }
358
359 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 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 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 PaymentMethod::Known(KnownMethod::Bolt11) => {
417 if let Some(ref bolt11_settings) = settings.bolt11 {
418 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 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 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 PaymentMethod::Known(KnownMethod::Bolt12) => {
459 if settings.bolt12.is_some() {
460 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, };
468 self.mint_info.nuts.nut04.methods.push(mint_method_settings);
469 self.mint_info.nuts.nut04.disabled = false;
470
471 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, };
479 self.mint_info.nuts.nut05.methods.push(melt_method_settings);
480 self.mint_info.nuts.nut05.disabled = false;
481 }
482 }
483 PaymentMethod::Custom(_) => {
485 if settings.custom.contains_key(method.as_str()) {
487 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 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, };
506 self.mint_info.nuts.nut05.methods.push(melt_method_settings);
507 self.mint_info.nuts.nut05.disabled = false;
508 }
509 }
510 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 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 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 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 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 pub async fn build_with_signatory(
577 #[allow(unused_mut)] mut self,
578 signatory: Arc<dyn Signatory + Send + Sync>,
579 ) -> Result<Mint, Error> {
580 let active_keysets = signatory.keysets().await?;
582
583 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 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 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 if keyset.amounts != *amounts {
617 tracing::info!("Rotating keyset for unit {} due to amounts mismatch", unit);
618 rotate = true;
619 }
620
621 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
743pub struct MintMeltLimits {
744 pub mint_min: Amount,
746 pub mint_max: Amount,
748 pub melt_min: Amount,
750 pub melt_max: Amount,
752}
753
754impl MintMeltLimits {
755 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 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 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 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 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 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 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, 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 assert_eq!(mint_info.nuts.nut15.methods.len(), 0);
1212
1213 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 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 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 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 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 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 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 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 let settings = SettingsResponse {
1358 unit: "usd".to_string(),
1359 bolt11: None,
1360 bolt12: None,
1361 onchain: None,
1362 custom: HashMap::new(), };
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 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 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 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 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 assert_eq!(mint_info.nuts.nut04.methods.len(), 2);
1582 assert_eq!(mint_info.nuts.nut05.methods.len(), 2);
1583 }
1584}