use std::collections::HashMap;
use std::sync::Arc;
use bitcoin::bip32::DerivationPath;
use cdk_common::database::{DynMintAuthDatabase, DynMintDatabase, MintKeysDatabase};
use cdk_common::error::Error;
use cdk_common::nut00::KnownMethod;
use cdk_common::nut04::MintMethodOptions;
use cdk_common::nut05::MeltMethodOptions;
use cdk_common::payment::DynMintPayment;
use cdk_common::{nut21, nut22};
use cdk_signatory::signatory::{RotateKeyArguments, Signatory};
use super::nut17::SupportedMethods;
use super::nut19::{self, CachedEndpoint};
use super::Nuts;
use crate::amount::Amount;
use crate::cdk_database;
use crate::mint::Mint;
use crate::nuts::{
ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion,
MppMethodSettings, PaymentMethod, ProtectedEndpoint,
};
use crate::types::PaymentProcessorKey;
#[derive(Debug, Clone)]
pub struct UnitConfig {
pub amounts: Vec<u64>,
pub input_fee_ppk: u64,
}
impl Default for UnitConfig {
fn default() -> Self {
Self {
amounts: (0..32).map(|i| 2_u64.pow(i)).collect(),
input_fee_ppk: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct KeysetRotation {
pub unit: CurrencyUnit,
pub amounts: Vec<u64>,
pub input_fee_ppk: u64,
pub use_keyset_v2: bool,
pub final_expiry: Option<u64>,
}
pub struct MintBuilder {
mint_info: MintInfo,
localstore: DynMintDatabase,
auth_localstore: Option<DynMintAuthDatabase>,
payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
supported_units: HashMap<CurrencyUnit, (u64, Vec<u64>)>,
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
use_keyset_v2: Option<bool>,
keyset_rotations: Vec<KeysetRotation>,
max_inputs: usize,
max_outputs: usize,
max_batch_size: Option<u64>,
}
impl std::fmt::Debug for MintBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MintBuilder")
.field("mint_info", &self.mint_info)
.field("supported_units", &self.supported_units)
.finish_non_exhaustive()
}
}
impl MintBuilder {
pub fn new(localstore: DynMintDatabase) -> MintBuilder {
let mint_info = MintInfo {
nuts: Nuts::new()
.nut07(true)
.nut08(true)
.nut09(true)
.nut10(true)
.nut11(true)
.nut12(true)
.nut14(true)
.nut20(true)
.nut29(cdk_common::nut29::Settings::default()),
..Default::default()
};
MintBuilder {
mint_info,
localstore,
auth_localstore: None,
payment_processors: HashMap::new(),
supported_units: HashMap::new(),
custom_paths: HashMap::new(),
use_keyset_v2: None,
keyset_rotations: Vec::new(),
max_inputs: 1000,
max_outputs: 1000,
max_batch_size: None,
}
}
pub fn with_keyset_v2(mut self, use_keyset_v2: Option<bool>) -> Self {
self.use_keyset_v2 = use_keyset_v2;
self
}
pub fn with_keyset_rotation(mut self, rotation: KeysetRotation) -> Self {
self.keyset_rotations.push(rotation);
self
}
pub fn with_auth(
mut self,
auth_localstore: DynMintAuthDatabase,
openid_discovery: String,
client_id: String,
protected_endpoints: Vec<ProtectedEndpoint>,
) -> Self {
self.auth_localstore = Some(auth_localstore);
self.mint_info.nuts.nut21 = Some(nut21::Settings::new(
openid_discovery,
client_id,
protected_endpoints,
));
self
}
pub async fn init_from_db_if_present(&mut self) -> Result<(), cdk_database::Error> {
let bytes_opt = self
.localstore
.kv_read(
super::CDK_MINT_PRIMARY_NAMESPACE,
super::CDK_MINT_CONFIG_SECONDARY_NAMESPACE,
super::CDK_MINT_CONFIG_KV_KEY,
)
.await?;
if let Some(bytes) = bytes_opt {
if let Ok(info) = serde_json::from_slice::<MintInfo>(&bytes) {
self.mint_info = info;
} else {
tracing::warn!("Failed to parse existing mint_info from DB; using builder state");
}
}
Ok(())
}
pub fn with_blind_auth(
mut self,
bat_max_mint: u64,
protected_endpoints: Vec<ProtectedEndpoint>,
) -> Self {
let mut nuts = self.mint_info.nuts;
nuts.nut22 = Some(nut22::Settings::new(bat_max_mint, protected_endpoints));
self.mint_info.nuts = nuts;
self
}
pub fn with_mint_info(mut self, mint_info: MintInfo) -> Self {
self.mint_info = mint_info;
self
}
pub fn with_name(mut self, name: String) -> Self {
self.mint_info.name = Some(name);
self
}
pub fn with_urls(mut self, urls: Vec<String>) -> Self {
self.mint_info.urls = Some(urls);
self
}
pub fn with_icon_url(mut self, icon_url: String) -> Self {
self.mint_info.icon_url = Some(icon_url);
self
}
pub fn with_motd(mut self, motd: String) -> Self {
self.mint_info.motd = Some(motd);
self
}
pub fn current_mint_info(&self) -> MintInfo {
self.mint_info.clone()
}
pub fn with_tos_url(mut self, tos_url: String) -> Self {
self.mint_info.tos_url = Some(tos_url);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.mint_info.description = Some(description);
self
}
pub fn with_long_description(mut self, description: String) -> Self {
self.mint_info.description_long = Some(description);
self
}
pub fn with_version(mut self, version: MintVersion) -> Self {
self.mint_info.version = Some(version);
self
}
pub fn with_contact_info(mut self, contact_info: ContactInfo) -> Self {
let mut contacts = self.mint_info.contact.clone().unwrap_or_default();
contacts.push(contact_info);
self.mint_info.contact = Some(contacts);
self
}
pub fn with_pubkey(mut self, pubkey: crate::nuts::PublicKey) -> Self {
self.mint_info.pubkey = Some(pubkey);
self
}
pub fn with_supported_websockets(mut self, supported_method: SupportedMethods) -> Self {
let mut supported_settings = self.mint_info.nuts.nut17.supported.clone();
if !supported_settings.contains(&supported_method) {
supported_settings.push(supported_method);
self.mint_info.nuts = self.mint_info.nuts.nut17(supported_settings);
}
self
}
pub fn with_cache(mut self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
let nut19_settings = nut19::Settings {
ttl,
cached_endpoints,
};
self.mint_info.nuts.nut19 = nut19_settings;
self
}
pub fn with_custom_derivation_paths(
mut self,
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
) -> Self {
self.custom_paths = custom_paths;
self
}
pub fn with_limits(mut self, max_inputs: usize, max_outputs: usize) -> Self {
self.max_inputs = max_inputs;
self.max_outputs = max_outputs;
self
}
pub fn with_batch_minting(
mut self,
max_batch_size: Option<u64>,
methods: Option<Vec<String>>,
) -> Self {
self.max_batch_size = max_batch_size;
self.mint_info.nuts.nut29 = cdk_common::nut29::Settings::new(max_batch_size, methods);
self
}
pub fn configure_unit(&mut self, unit: CurrencyUnit, config: UnitConfig) -> Result<(), Error> {
if config.amounts.is_empty() {
return Err(Error::Custom("Amounts list cannot be empty".to_string()));
}
let mut sorted = config.amounts.clone();
sorted.sort_unstable();
sorted.dedup();
if sorted.len() != config.amounts.len() {
return Err(Error::Custom(
"Amounts list contains duplicates".to_string(),
));
}
if sorted != config.amounts {
return Err(Error::Custom(
"Amounts must be sorted in ascending order".to_string(),
));
}
if config.amounts.contains(&0) {
return Err(Error::Custom("Amounts must be greater than 0".to_string()));
}
self.supported_units
.insert(unit, (config.input_fee_ppk, config.amounts));
Ok(())
}
pub async fn add_payment_processor(
&mut self,
unit: CurrencyUnit,
method: PaymentMethod,
limits: MintMeltLimits,
payment_processor: DynMintPayment,
) -> Result<(), Error> {
let key = PaymentProcessorKey {
unit: unit.clone(),
method: method.clone(),
};
let settings = payment_processor.get_settings().await?;
match method {
PaymentMethod::Known(KnownMethod::Bolt11) => {
if let Some(ref bolt11_settings) = settings.bolt11 {
if bolt11_settings.mpp {
let mpp_settings = MppMethodSettings {
method: method.clone(),
unit: unit.clone(),
};
let mut mpp = self.mint_info.nuts.nut15.clone();
mpp.methods.push(mpp_settings);
self.mint_info.nuts.nut15 = mpp;
}
let mint_method_settings = MintMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.mint_min),
max_amount: Some(limits.mint_max),
options: Some(MintMethodOptions::Bolt11 {
description: bolt11_settings.invoice_description,
}),
};
self.mint_info.nuts.nut04.methods.push(mint_method_settings);
self.mint_info.nuts.nut04.disabled = false;
let melt_method_settings = MeltMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.melt_min),
max_amount: Some(limits.melt_max),
options: Some(MeltMethodOptions::Bolt11 {
amountless: bolt11_settings.amountless,
}),
};
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
self.mint_info.nuts.nut05.disabled = false;
}
}
PaymentMethod::Known(KnownMethod::Bolt12) => {
if settings.bolt12.is_some() {
let mint_method_settings = MintMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.mint_min),
max_amount: Some(limits.mint_max),
options: None, };
self.mint_info.nuts.nut04.methods.push(mint_method_settings);
self.mint_info.nuts.nut04.disabled = false;
let melt_method_settings = MeltMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.melt_min),
max_amount: Some(limits.melt_max),
options: None, };
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
self.mint_info.nuts.nut05.disabled = false;
}
}
PaymentMethod::Custom(_) => {
if settings.custom.contains_key(method.as_str()) {
let mint_method_settings = MintMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.mint_min),
max_amount: Some(limits.mint_max),
options: Some(MintMethodOptions::Custom {}),
};
self.mint_info.nuts.nut04.methods.push(mint_method_settings);
self.mint_info.nuts.nut04.disabled = false;
let melt_method_settings = MeltMethodSettings {
method: method.clone(),
unit: unit.clone(),
min_amount: Some(limits.melt_min),
max_amount: Some(limits.melt_max),
options: None, };
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
self.mint_info.nuts.nut05.disabled = false;
}
}
}
if !self.supported_units.contains_key(&key.unit) {
self.configure_unit(key.unit.clone(), Default::default())?;
}
self.payment_processors.insert(key, payment_processor);
Ok(())
}
pub fn set_unit_fee(&mut self, unit: &CurrencyUnit, input_fee_ppk: u64) -> Result<(), Error> {
let (input_fee, _) = self
.supported_units
.get_mut(unit)
.ok_or(Error::UnsupportedUnit)?;
*input_fee = input_fee_ppk;
Ok(())
}
pub async fn build_with_signatory(
#[allow(unused_mut)] mut self,
signatory: Arc<dyn Signatory + Send + Sync>,
) -> Result<Mint, Error> {
let active_keysets = signatory.keysets().await?;
if self.auth_localstore.is_some() {
self.supported_units
.entry(CurrencyUnit::Auth)
.or_insert((0, vec![1]));
}
for (unit, (fee, amounts)) in &self.supported_units {
let keyset = active_keysets
.keysets
.iter()
.find(|k| k.active && k.unit == *unit);
let mut rotate = false;
if let Some(keyset) = keyset {
if keyset.input_fee_ppk != *fee {
tracing::info!(
"Rotating keyset for unit {} due to fee mismatch (current: {}, expected: {})",
unit,
keyset.input_fee_ppk,
fee
);
rotate = true;
}
if keyset.amounts != *amounts {
tracing::info!("Rotating keyset for unit {} due to amounts mismatch", unit);
rotate = true;
}
if let Some(want_v2) = self.use_keyset_v2 {
let is_v2 =
keyset.id.get_version() == cdk_common::nut02::KeySetVersion::Version01;
if want_v2 && !is_v2 {
tracing::info!("Rotating keyset for unit {} due to explicit V2 preference (current is V1)", unit);
rotate = true;
} else if !want_v2 && is_v2 {
tracing::info!("Rotating keyset for unit {} due to explicit V1 preference (current is V2)", unit);
rotate = true;
}
}
} else {
tracing::info!("Rotating keyset for unit {} (no active keyset found)", unit);
rotate = true;
}
if rotate {
signatory
.rotate_keyset(RotateKeyArguments {
unit: unit.clone(),
amounts: amounts.clone(),
input_fee_ppk: *fee,
keyset_id_type: if self.use_keyset_v2.unwrap_or(true) {
cdk_common::nut02::KeySetVersion::Version01
} else {
cdk_common::nut02::KeySetVersion::Version00
},
final_expiry: None,
})
.await?;
}
}
for rotation in &self.keyset_rotations {
signatory
.rotate_keyset(RotateKeyArguments {
unit: rotation.unit.clone(),
amounts: rotation.amounts.clone(),
input_fee_ppk: rotation.input_fee_ppk,
keyset_id_type: if rotation.use_keyset_v2 {
cdk_common::nut02::KeySetVersion::Version01
} else {
cdk_common::nut02::KeySetVersion::Version00
},
final_expiry: rotation.final_expiry,
})
.await?;
}
if let Some(auth_localstore) = self.auth_localstore {
return Mint::new_with_auth(
self.mint_info,
signatory,
self.localstore,
auth_localstore,
self.payment_processors,
self.max_inputs,
self.max_outputs,
)
.await;
}
Mint::new(
self.mint_info,
signatory,
self.localstore,
self.payment_processors,
self.max_inputs,
self.max_outputs,
)
.await
}
pub async fn build_with_seed(
self,
keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
seed: &[u8],
) -> Result<Mint, Error> {
let in_memory_signatory = cdk_signatory::db_signatory::DbSignatory::new(
keystore,
seed,
self.supported_units.clone(),
self.custom_paths.clone(),
)
.await?;
let signatory = Arc::new(cdk_signatory::embedded::Service::new(Arc::new(
in_memory_signatory,
)));
self.build_with_signatory(signatory).await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MintMeltLimits {
pub mint_min: Amount,
pub mint_max: Amount,
pub melt_min: Amount,
pub melt_max: Amount,
}
impl MintMeltLimits {
pub fn new(min: u64, max: u64) -> Self {
Self {
mint_min: min.into(),
mint_max: max.into(),
melt_min: min.into(),
melt_max: max.into(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
use async_trait::async_trait;
use cdk_common::payment::{
Bolt11Settings, Bolt12Settings, CreateIncomingPaymentResponse, Event,
IncomingPaymentOptions, MakePaymentResponse, OutgoingPaymentOptions, PaymentIdentifier,
PaymentQuoteResponse, SettingsResponse,
};
use cdk_sqlite::mint::memory;
use futures::Stream;
use KnownMethod;
use super::*;
struct MockPaymentProcessor {
settings: SettingsResponse,
}
#[async_trait]
impl cdk_common::payment::MintPayment for MockPaymentProcessor {
type Err = cdk_common::payment::Error;
async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
Ok(self.settings.clone())
}
async fn create_incoming_payment_request(
&self,
_options: IncomingPaymentOptions,
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
unimplemented!()
}
async fn get_payment_quote(
&self,
_unit: &CurrencyUnit,
_options: OutgoingPaymentOptions,
) -> Result<PaymentQuoteResponse, Self::Err> {
unimplemented!()
}
async fn make_payment(
&self,
_unit: &CurrencyUnit,
_options: OutgoingPaymentOptions,
) -> Result<MakePaymentResponse, Self::Err> {
unimplemented!()
}
async fn wait_payment_event(
&self,
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
unimplemented!()
}
fn is_wait_invoice_active(&self) -> bool {
false
}
fn cancel_wait_invoice(&self) {}
async fn check_incoming_payment_status(
&self,
_payment_identifier: &PaymentIdentifier,
) -> Result<Vec<cdk_common::payment::WaitPaymentResponse>, Self::Err> {
unimplemented!()
}
async fn check_outgoing_payment(
&self,
_payment_identifier: &PaymentIdentifier,
) -> Result<MakePaymentResponse, Self::Err> {
unimplemented!()
}
}
#[tokio::test]
async fn test_mint_builder_default_nuts_support() {
let localstore = Arc::new(memory::empty().await.unwrap());
let builder = MintBuilder::new(localstore);
let mint_info = builder.current_mint_info();
assert!(
mint_info.nuts.nut07.supported,
"NUT-07 should be supported by default"
);
assert!(
mint_info.nuts.nut08.supported,
"NUT-08 should be supported by default"
);
assert!(
mint_info.nuts.nut09.supported,
"NUT-09 should be supported by default"
);
assert!(
mint_info.nuts.nut10.supported,
"NUT-10 should be supported by default"
);
assert!(
mint_info.nuts.nut11.supported,
"NUT-11 should be supported by default"
);
assert!(
mint_info.nuts.nut12.supported,
"NUT-12 should be supported by default"
);
assert!(
mint_info.nuts.nut14.supported,
"NUT-14 (HTLC) should be supported by default"
);
assert!(
mint_info.nuts.nut20.supported,
"NUT-20 should be supported by default"
);
assert!(
mint_info.nuts.nut29.is_empty(),
"NUT-29 should have empty settings by default"
);
}
#[tokio::test]
async fn test_mint_builder_batch_minting_settings() {
let localstore = Arc::new(memory::empty().await.unwrap());
let builder = MintBuilder::new(localstore).with_batch_minting(
Some(100),
Some(vec!["bolt11".to_string(), "bolt12".to_string()]),
);
let mint_info = builder.current_mint_info();
assert_eq!(
mint_info.nuts.nut29.max_batch_size,
Some(100),
"NUT-29 max_batch_size should be set"
);
assert_eq!(
mint_info.nuts.nut29.methods,
Some(vec!["bolt11".to_string(), "bolt12".to_string()]),
"NUT-29 methods should be set"
);
}
#[tokio::test]
async fn test_add_payment_processor_bolt11() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Sat,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let bolt11_settings = Bolt11Settings {
mpp: true,
amountless: true,
invoice_description: true,
};
let settings = SettingsResponse {
unit: "sat".to_string(),
bolt11: Some(bolt11_settings),
bolt12: None,
custom: HashMap::new(),
};
let payment_processor = Arc::new(MockPaymentProcessor { settings });
let unit = CurrencyUnit::Sat;
let method = PaymentMethod::Known(KnownMethod::Bolt11);
let limits = MintMeltLimits::new(100, 10000);
builder
.add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert!(!mint_info.nuts.nut04.disabled);
assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
let mint_method = &mint_info.nuts.nut04.methods[0];
assert_eq!(mint_method.method, method);
assert_eq!(mint_method.unit, unit);
assert_eq!(mint_method.min_amount, Some(limits.mint_min));
assert_eq!(mint_method.max_amount, Some(limits.mint_max));
assert!(matches!(
mint_method.options,
Some(MintMethodOptions::Bolt11 { description: true })
));
assert!(!mint_info.nuts.nut05.disabled);
assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
let melt_method = &mint_info.nuts.nut05.methods[0];
assert_eq!(melt_method.method, method);
assert_eq!(melt_method.unit, unit);
assert_eq!(melt_method.min_amount, Some(limits.melt_min));
assert_eq!(melt_method.max_amount, Some(limits.melt_max));
assert!(matches!(
melt_method.options,
Some(MeltMethodOptions::Bolt11 { amountless: true })
));
assert_eq!(mint_info.nuts.nut15.methods.len(), 1);
let mpp_method = &mint_info.nuts.nut15.methods[0];
assert_eq!(mpp_method.method, method);
assert_eq!(mpp_method.unit, unit);
}
#[tokio::test]
async fn test_add_payment_processor_bolt11_without_mpp() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Sat,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let bolt11_settings = Bolt11Settings {
mpp: false, amountless: false,
invoice_description: false,
};
let settings = SettingsResponse {
unit: "sat".to_string(),
bolt11: Some(bolt11_settings),
bolt12: None,
custom: HashMap::new(),
};
let payment_processor = Arc::new(MockPaymentProcessor { settings });
let unit = CurrencyUnit::Sat;
let method = PaymentMethod::Known(KnownMethod::Bolt11);
let limits = MintMeltLimits::new(100, 10000);
builder
.add_payment_processor(unit, method, limits, payment_processor)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert_eq!(mint_info.nuts.nut15.methods.len(), 0);
assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
}
#[tokio::test]
async fn test_add_payment_processor_bolt12() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Sat,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let bolt12_settings = Bolt12Settings { amountless: true };
let settings = SettingsResponse {
unit: "sat".to_string(),
bolt11: None,
bolt12: Some(bolt12_settings),
custom: HashMap::new(),
};
let payment_processor = Arc::new(MockPaymentProcessor { settings });
let unit = CurrencyUnit::Sat;
let method = PaymentMethod::Known(KnownMethod::Bolt12);
let limits = MintMeltLimits::new(100, 10000);
builder
.add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert!(!mint_info.nuts.nut04.disabled);
assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
let mint_method = &mint_info.nuts.nut04.methods[0];
assert_eq!(mint_method.method, method);
assert_eq!(mint_method.unit, unit);
assert_eq!(mint_method.min_amount, Some(limits.mint_min));
assert_eq!(mint_method.max_amount, Some(limits.mint_max));
assert!(mint_method.options.is_none());
assert!(!mint_info.nuts.nut05.disabled);
assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
let melt_method = &mint_info.nuts.nut05.methods[0];
assert_eq!(melt_method.method, method);
assert_eq!(melt_method.unit, unit);
assert_eq!(melt_method.min_amount, Some(limits.melt_min));
assert_eq!(melt_method.max_amount, Some(limits.melt_max));
assert!(melt_method.options.is_none());
}
#[tokio::test]
async fn test_add_payment_processor_custom() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Usd,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let mut custom_methods = HashMap::new();
custom_methods.insert("paypal".to_string(), "{}".to_string());
let settings = SettingsResponse {
unit: "usd".to_string(),
bolt11: None,
bolt12: None,
custom: custom_methods,
};
let payment_processor = Arc::new(MockPaymentProcessor { settings });
let unit = CurrencyUnit::Usd;
let method = PaymentMethod::Custom("paypal".to_string());
let limits = MintMeltLimits::new(100, 10000);
builder
.add_payment_processor(unit.clone(), method.clone(), limits, payment_processor)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert!(!mint_info.nuts.nut04.disabled);
assert_eq!(mint_info.nuts.nut04.methods.len(), 1);
let mint_method = &mint_info.nuts.nut04.methods[0];
assert_eq!(mint_method.method, method);
assert_eq!(mint_method.unit, unit);
assert_eq!(mint_method.min_amount, Some(limits.mint_min));
assert_eq!(mint_method.max_amount, Some(limits.mint_max));
assert!(matches!(
mint_method.options,
Some(MintMethodOptions::Custom {})
));
assert!(!mint_info.nuts.nut05.disabled);
assert_eq!(mint_info.nuts.nut05.methods.len(), 1);
let melt_method = &mint_info.nuts.nut05.methods[0];
assert_eq!(melt_method.method, method);
assert_eq!(melt_method.unit, unit);
assert_eq!(melt_method.min_amount, Some(limits.melt_min));
assert_eq!(melt_method.max_amount, Some(limits.melt_max));
assert!(melt_method.options.is_none());
}
#[tokio::test]
async fn test_add_payment_processor_custom_not_supported() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Usd,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let settings = SettingsResponse {
unit: "usd".to_string(),
bolt11: None,
bolt12: None,
custom: HashMap::new(), };
let payment_processor = Arc::new(MockPaymentProcessor { settings });
let unit = CurrencyUnit::Usd;
let method = PaymentMethod::Custom("paypal".to_string());
let limits = MintMeltLimits::new(1, 1000);
builder
.add_payment_processor(unit, method, limits, payment_processor)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert_eq!(mint_info.nuts.nut04.methods.len(), 0);
assert_eq!(mint_info.nuts.nut05.methods.len(), 0);
}
#[tokio::test]
async fn test_add_multiple_payment_processors() {
let localstore = Arc::new(memory::empty().await.unwrap());
let mut builder = MintBuilder::new(localstore);
builder
.configure_unit(
CurrencyUnit::Sat,
UnitConfig {
amounts: vec![1, 2, 4, 8, 16, 32],
input_fee_ppk: 0,
},
)
.unwrap();
let bolt11_settings = Bolt11Settings {
mpp: false,
amountless: true,
invoice_description: false,
};
let settings1 = SettingsResponse {
unit: "sat".to_string(),
bolt11: Some(bolt11_settings),
bolt12: None,
custom: HashMap::new(),
};
let processor1 = Arc::new(MockPaymentProcessor {
settings: settings1,
});
builder
.add_payment_processor(
CurrencyUnit::Sat,
PaymentMethod::Known(KnownMethod::Bolt11),
MintMeltLimits::new(100, 10000),
processor1,
)
.await
.unwrap();
let bolt12_settings = Bolt12Settings { amountless: false };
let settings2 = SettingsResponse {
unit: "sat".to_string(),
bolt11: None,
bolt12: Some(bolt12_settings),
custom: HashMap::new(),
};
let processor2 = Arc::new(MockPaymentProcessor {
settings: settings2,
});
builder
.add_payment_processor(
CurrencyUnit::Sat,
PaymentMethod::Known(KnownMethod::Bolt12),
MintMeltLimits::new(200, 20000),
processor2,
)
.await
.unwrap();
let mint_info = builder.current_mint_info();
assert_eq!(mint_info.nuts.nut04.methods.len(), 2);
assert_eq!(mint_info.nuts.nut05.methods.len(), 2);
}
}