Эх сурвалжийг харах

feat(wallet): remove multi mint wallet (#1582)

* feat(wallet): replacing multi mint wallet with wallet repository
asmo 1 долоо хоног өмнө
parent
commit
b809131544
44 өөрчлөгдсөн 2648 нэмэгдсэн , 5447 устгасан
  1. 51 53
      crates/cdk-cli/src/main.rs
  2. 26 17
      crates/cdk-cli/src/sub_commands/balance.rs
  3. 6 8
      crates/cdk-cli/src/sub_commands/burn.rs
  4. 6 9
      crates/cdk-cli/src/sub_commands/cat_device_login.rs
  5. 6 9
      crates/cdk-cli/src/sub_commands/cat_login.rs
  6. 3 3
      crates/cdk-cli/src/sub_commands/check_pending.rs
  7. 7 5
      crates/cdk-cli/src/sub_commands/create_request.rs
  8. 5 5
      crates/cdk-cli/src/sub_commands/list_mint_proofs.rs
  9. 303 284
      crates/cdk-cli/src/sub_commands/melt.rs
  10. 5 4
      crates/cdk-cli/src/sub_commands/mint.rs
  11. 45 52
      crates/cdk-cli/src/sub_commands/mint_blind_auth.rs
  12. 38 30
      crates/cdk-cli/src/sub_commands/npubcash.rs
  13. 3 3
      crates/cdk-cli/src/sub_commands/pay_request.rs
  14. 11 4
      crates/cdk-cli/src/sub_commands/pending_mints.rs
  15. 28 38
      crates/cdk-cli/src/sub_commands/receive.rs
  16. 7 13
      crates/cdk-cli/src/sub_commands/restore.rs
  17. 32 88
      crates/cdk-cli/src/sub_commands/send.rs
  18. 95 74
      crates/cdk-cli/src/sub_commands/transfer.rs
  19. 10 5
      crates/cdk-cli/src/sub_commands/update_mint_url.rs
  20. 12 14
      crates/cdk-cli/src/utils.rs
  21. 3 1
      crates/cdk-common/src/wallet/mod.rs
  22. 2 2
      crates/cdk-ffi/src/lib.rs
  23. 0 1206
      crates/cdk-ffi/src/multi_mint_wallet.rs
  24. 1 10
      crates/cdk-ffi/src/types/payment_request.rs
  25. 1 0
      crates/cdk-ffi/src/wallet.rs
  26. 277 0
      crates/cdk-ffi/src/wallet_repository.rs
  27. 66 22
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  28. 323 241
      crates/cdk-integration-tests/tests/wallet_repository.rs
  29. 12 9
      crates/cdk/examples/configure_wallet.rs
  30. 60 34
      crates/cdk/examples/multi-mint-wallet.rs
  31. 17 12
      crates/cdk/examples/multimint-npubcash.rs
  32. 18 15
      crates/cdk/examples/nostr_backup.rs
  33. 30 14
      crates/cdk/examples/payment_request.rs
  34. 58 39
      crates/cdk/examples/revoke_send.rs
  35. 20 10
      crates/cdk/examples/token-proofs.rs
  36. 8 0
      crates/cdk/src/wallet/issue/mod.rs
  37. 5 1
      crates/cdk/src/wallet/melt/mod.rs
  38. 4 2
      crates/cdk/src/wallet/mod.rs
  39. 0 3068
      crates/cdk/src/wallet/multi_mint_wallet.rs
  40. 4 3
      crates/cdk/src/wallet/nostr_backup.rs
  41. 63 31
      crates/cdk/src/wallet/payment_request.rs
  42. 4 6
      crates/cdk/src/wallet/streams/npubcash.rs
  43. 970 0
      crates/cdk/src/wallet/wallet_repository.rs
  44. 3 3
      misc/fake_itests.sh

+ 51 - 53
crates/cdk-cli/src/main.rs

@@ -11,7 +11,6 @@ use bip39::Mnemonic;
 use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::MultiMintWallet;
 #[cfg(feature = "redb")]
 use cdk_redb::WalletRedbDatabase;
 use cdk_sqlite::WalletSqliteDatabase;
@@ -210,109 +209,108 @@ async fn main() -> Result<()> {
     let currency_unit = CurrencyUnit::from_str(&args.unit)
         .unwrap_or_else(|_| CurrencyUnit::Custom(args.unit.clone()));
 
-    // Create MultiMintWallet with specified currency unit
-    // The constructor will automatically load wallets for this currency unit
-    let multi_mint_wallet = match &args.proxy {
-        Some(proxy_url) => {
-            MultiMintWallet::new_with_proxy(
-                localstore.clone(),
-                seed,
-                currency_unit.clone(),
-                proxy_url.clone(),
-            )
-            .await?
+    // Create WalletRepository using builder pattern
+    let wallet_repository = {
+        let mut builder = cdk::wallet::WalletRepositoryBuilder::new()
+            .localstore(localstore.clone())
+            .seed(seed);
+
+        if let Some(proxy_url) = &args.proxy {
+            builder = builder.proxy_url(proxy_url.clone());
         }
-        None => {
-            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-            {
-                match args.transport {
-                    TorToggle::On => {
-                        MultiMintWallet::new_with_tor(
-                            localstore.clone(),
-                            seed,
-                            currency_unit.clone(),
-                        )
-                        .await?
-                    }
-                    TorToggle::Off => {
-                        MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone())
-                            .await?
-                    }
-                }
-            }
-            #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
-            {
-                MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone()).await?
-            }
+
+        #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+        if matches!(args.transport, TorToggle::On) {
+            builder = builder.tor();
         }
+
+        builder.build().await?
     };
 
     match &args.command {
         Commands::DecodeToken(sub_command_args) => {
             sub_commands::decode_token::decode_token(sub_command_args)
         }
-        Commands::Balance => sub_commands::balance::balance(&multi_mint_wallet).await,
+        Commands::Balance => sub_commands::balance::balance(&wallet_repository).await,
         Commands::Melt(sub_command_args) => {
-            sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await
+            sub_commands::melt::pay(&wallet_repository, sub_command_args, &currency_unit).await
         }
         Commands::Receive(sub_command_args) => {
-            sub_commands::receive::receive(&multi_mint_wallet, sub_command_args, &work_dir).await
+            sub_commands::receive::receive(
+                &wallet_repository,
+                sub_command_args,
+                &work_dir,
+                &currency_unit,
+            )
+            .await
         }
         Commands::Send(sub_command_args) => {
-            sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
+            sub_commands::send::send(&wallet_repository, sub_command_args, &currency_unit).await
         }
         Commands::Transfer(sub_command_args) => {
-            sub_commands::transfer::transfer(&multi_mint_wallet, sub_command_args).await
+            sub_commands::transfer::transfer(&wallet_repository, sub_command_args, &currency_unit)
+                .await
         }
         Commands::CheckPending => {
-            sub_commands::check_pending::check_pending(&multi_mint_wallet).await
+            sub_commands::check_pending::check_pending(&wallet_repository).await
         }
         Commands::MintInfo(sub_command_args) => {
             sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
         }
         Commands::Mint(sub_command_args) => {
-            sub_commands::mint::mint(&multi_mint_wallet, sub_command_args).await
+            sub_commands::mint::mint(&wallet_repository, sub_command_args, &currency_unit).await
         }
         Commands::MintPending => {
-            sub_commands::pending_mints::mint_pending(&multi_mint_wallet).await
+            sub_commands::pending_mints::mint_pending(&wallet_repository).await
         }
         Commands::Burn(sub_command_args) => {
-            sub_commands::burn::burn(&multi_mint_wallet, sub_command_args).await
+            sub_commands::burn::burn(&wallet_repository, sub_command_args).await
         }
         Commands::Restore(sub_command_args) => {
-            sub_commands::restore::restore(&multi_mint_wallet, sub_command_args).await
+            sub_commands::restore::restore(&wallet_repository, sub_command_args, &currency_unit)
+                .await
         }
         Commands::UpdateMintUrl(sub_command_args) => {
-            sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args)
-                .await
+            sub_commands::update_mint_url::update_mint_url(
+                &wallet_repository,
+                sub_command_args,
+                &currency_unit,
+            )
+            .await
         }
         Commands::ListMintProofs => {
-            sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await
+            sub_commands::list_mint_proofs::proofs(&wallet_repository).await
         }
         Commands::DecodeRequest(sub_command_args) => {
             sub_commands::decode_request::decode_payment_request(sub_command_args)
         }
         Commands::PayRequest(sub_command_args) => {
-            sub_commands::pay_request::pay_request(&multi_mint_wallet, sub_command_args).await
+            sub_commands::pay_request::pay_request(&wallet_repository, sub_command_args).await
         }
         Commands::CreateRequest(sub_command_args) => {
-            sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
+            sub_commands::create_request::create_request(
+                &wallet_repository,
+                sub_command_args,
+                &currency_unit,
+            )
+            .await
         }
         Commands::MintBlindAuth(sub_command_args) => {
             sub_commands::mint_blind_auth::mint_blind_auth(
-                &multi_mint_wallet,
+                &wallet_repository,
                 sub_command_args,
                 &work_dir,
+                &currency_unit,
             )
             .await
         }
         Commands::CatLogin(sub_command_args) => {
-            sub_commands::cat_login::cat_login(&multi_mint_wallet, sub_command_args, &work_dir)
+            sub_commands::cat_login::cat_login(&wallet_repository, sub_command_args, &work_dir)
                 .await
         }
         Commands::CatDeviceLogin(sub_command_args) => {
             sub_commands::cat_device_login::cat_device_login(
-                &multi_mint_wallet,
+                &wallet_repository,
                 sub_command_args,
                 &work_dir,
             )
@@ -321,7 +319,7 @@ async fn main() -> Result<()> {
         #[cfg(feature = "npubcash")]
         Commands::NpubCash { mint_url, command } => {
             sub_commands::npubcash::npubcash(
-                &multi_mint_wallet,
+                &wallet_repository,
                 mint_url,
                 command,
                 Some(args.npubcash_url.clone()),

+ 26 - 17
crates/cdk-cli/src/sub_commands/balance.rs

@@ -3,43 +3,52 @@ use std::collections::BTreeMap;
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Amount;
+use cdk_common::wallet::WalletKey;
 
-pub async fn balance(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
+pub async fn balance(wallet_repository: &WalletRepository) -> Result<()> {
     // Show individual mint balances
-    let mint_balances = mint_balances(multi_mint_wallet, multi_mint_wallet.unit()).await?;
+    let mint_balances = mint_balances(wallet_repository).await?;
 
-    // Show total balance using the new unified interface
-    let total = multi_mint_wallet.total_balance().await?;
     if !mint_balances.is_empty() {
+        // Aggregate totals per currency unit
+        let mut unit_totals: BTreeMap<CurrencyUnit, Amount> = BTreeMap::new();
+        for (_, unit, amount) in &mint_balances {
+            *unit_totals.entry(unit.clone()).or_insert(Amount::ZERO) += *amount;
+        }
+
         println!();
-        println!(
-            "Total balance across all wallets: {} {}",
-            total,
-            multi_mint_wallet.unit()
-        );
+        if unit_totals.len() == 1 {
+            if let Some((unit, total)) = unit_totals.into_iter().next() {
+                println!("Total balance across all wallets: {} {}", total, unit);
+            }
+        } else {
+            println!("Total balance across all wallets:");
+            for (unit, total) in &unit_totals {
+                println!("  {} {}", total, unit);
+            }
+        }
     }
 
     Ok(())
 }
 
 pub async fn mint_balances(
-    multi_mint_wallet: &MultiMintWallet,
-    unit: &CurrencyUnit,
-) -> Result<Vec<(MintUrl, Amount)>> {
-    let wallets: BTreeMap<MintUrl, Amount> = multi_mint_wallet.get_balances().await?;
+    wallet_repository: &WalletRepository,
+) -> Result<Vec<(MintUrl, CurrencyUnit, Amount)>> {
+    let wallets = wallet_repository.get_balances().await?;
 
     let mut wallets_vec = Vec::with_capacity(wallets.len());
 
-    for (i, (mint_url, amount)) in wallets
+    for (i, (wallet_key, amount)) in wallets
         .iter()
         .filter(|(_, a)| a > &&Amount::ZERO)
         .enumerate()
     {
-        let mint_url = mint_url.clone();
+        let WalletKey { mint_url, unit } = wallet_key.clone();
         println!("{i}: {mint_url} {amount} {unit}");
-        wallets_vec.push((mint_url, *amount))
+        wallets_vec.push((mint_url, unit, *amount))
     }
     Ok(wallets_vec)
 }

+ 6 - 8
crates/cdk-cli/src/sub_commands/burn.rs

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Amount;
 use clap::Args;
 
@@ -11,21 +11,19 @@ pub struct BurnSubCommand {
 }
 
 pub async fn burn(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &BurnSubCommand,
 ) -> Result<()> {
     let mut total_burnt = Amount::ZERO;
 
     match &sub_command_args.mint_url {
         Some(mint_url) => {
-            let wallet = multi_mint_wallet
-                .get_wallet(mint_url)
-                .await
-                .ok_or_else(|| anyhow::anyhow!("Wallet not found for mint: {}", mint_url))?;
-            total_burnt = wallet.check_all_pending_proofs().await?;
+            for wallet in wallet_repository.get_wallets_for_mint(mint_url).await {
+                total_burnt += wallet.check_all_pending_proofs().await?;
+            }
         }
         None => {
-            for wallet in multi_mint_wallet.get_wallets().await {
+            for wallet in wallet_repository.get_wallets().await {
                 let amount_burnt = wallet.check_all_pending_proofs().await?;
                 total_burnt += amount_burnt;
             }

+ 6 - 9
crates/cdk-cli/src/sub_commands/cat_device_login.rs

@@ -1,10 +1,10 @@
 use std::path::Path;
 use std::time::Duration;
 
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::MintInfo;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::OidcClient;
 use clap::Args;
 use serde::{Deserialize, Serialize};
@@ -19,21 +19,18 @@ pub struct CatDeviceLoginSubCommand {
 }
 
 pub async fn cat_device_login(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &CatDeviceLoginSubCommand,
     work_dir: &Path,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
     // Ensure the mint exists
-    if !multi_mint_wallet.has_mint(&mint_url).await {
-        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    if !wallet_repository.has_mint(&mint_url).await {
+        wallet_repository.add_wallet(mint_url.clone()).await?;
     }
 
-    let mint_info = multi_mint_wallet
-        .fetch_mint_info(&mint_url)
-        .await?
-        .ok_or(anyhow!("Mint info not found"))?;
+    let mint_info = wallet_repository.fetch_mint_info(&mint_url).await?;
 
     let (access_token, refresh_token) = get_device_code_token(&mint_info).await;
 

+ 6 - 9
crates/cdk-cli/src/sub_commands/cat_login.rs

@@ -1,9 +1,9 @@
 use std::path::Path;
 
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::MintInfo;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::OidcClient;
 use clap::Args;
 use serde::{Deserialize, Serialize};
@@ -21,21 +21,18 @@ pub struct CatLoginSubCommand {
 }
 
 pub async fn cat_login(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &CatLoginSubCommand,
     work_dir: &Path,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
     // Ensure the mint exists
-    if !multi_mint_wallet.has_mint(&mint_url).await {
-        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    if !wallet_repository.has_mint(&mint_url).await {
+        wallet_repository.add_wallet(mint_url.clone()).await?;
     }
 
-    let mint_info = multi_mint_wallet
-        .fetch_mint_info(&mint_url)
-        .await?
-        .ok_or(anyhow!("Mint info not found"))?;
+    let mint_info = wallet_repository.fetch_mint_info(&mint_url).await?;
 
     let (access_token, refresh_token) = get_access_token(
         &mint_info,

+ 3 - 3
crates/cdk-cli/src/sub_commands/check_pending.rs

@@ -1,9 +1,9 @@
 use anyhow::Result;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Amount;
 
-pub async fn check_pending(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
-    let wallets = multi_mint_wallet.get_wallets().await;
+pub async fn check_pending(wallet_repository: &WalletRepository) -> Result<()> {
+    let wallets = wallet_repository.get_wallets().await;
 
     for (i, wallet) in wallets.iter().enumerate() {
         let mint_url = wallet.mint_url.clone();

+ 7 - 5
crates/cdk-cli/src/sub_commands/create_request.rs

@@ -1,5 +1,6 @@
 use anyhow::Result;
-use cdk::wallet::{payment_request as pr, MultiMintWallet};
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::{payment_request as pr, WalletRepository};
 use clap::Args;
 
 #[derive(Args)]
@@ -39,13 +40,14 @@ pub struct CreateRequestSubCommand {
 }
 
 pub async fn create_request(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &CreateRequestSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     // Gather parameters for library call
     let params = pr::CreateRequestParams {
         amount: sub_command_args.amount,
-        unit: multi_mint_wallet.unit().to_string(),
+        unit: unit.to_string(),
         description: sub_command_args.description.clone(),
         pubkeys: sub_command_args.pubkey.clone(),
         num_sigs: sub_command_args.num_sigs,
@@ -56,7 +58,7 @@ pub async fn create_request(
         nostr_relays: sub_command_args.nostr_relay.clone(),
     };
 
-    let (req, nostr_wait) = multi_mint_wallet.create_request(params).await?;
+    let (req, nostr_wait) = wallet_repository.create_request(params).await?;
 
     // Print the request to stdout
     println!("{}", req);
@@ -64,7 +66,7 @@ pub async fn create_request(
     // If we set up Nostr transport, optionally wait for payment and receive it
     if let Some(info) = nostr_wait {
         println!("Listening for payment via Nostr...");
-        let amount = multi_mint_wallet.wait_for_nostr_payment(info).await?;
+        let amount = wallet_repository.wait_for_nostr_payment(info).await?;
         println!("Received {}", amount);
     }
 

+ 5 - 5
crates/cdk-cli/src/sub_commands/list_mint_proofs.rs

@@ -1,19 +1,19 @@
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, Proof};
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 
-pub async fn proofs(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
-    list_proofs(multi_mint_wallet).await?;
+pub async fn proofs(wallet_repository: &WalletRepository) -> Result<()> {
+    list_proofs(wallet_repository).await?;
     Ok(())
 }
 
 async fn list_proofs(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
 ) -> Result<Vec<(MintUrl, (Vec<Proof>, CurrencyUnit))>> {
     let mut proofs_vec = Vec::new();
 
-    let wallets = multi_mint_wallet.get_wallets().await;
+    let wallets = wallet_repository.get_wallets().await;
 
     for (i, wallet) in wallets.iter().enumerate() {
         let mint_url = wallet.mint_url.clone();

+ 303 - 284
crates/cdk-cli/src/sub_commands/melt.rs

@@ -1,15 +1,18 @@
+use std::collections::HashMap;
 use std::str::FromStr;
 
 use anyhow::{bail, Result};
 use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
 use cdk::mint_url::MintUrl;
+use cdk::nuts::nut00::KnownMethod;
 use cdk::nuts::{CurrencyUnit, MeltOptions, PaymentMethod};
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Bolt11Invoice;
+use cdk_common::wallet::WalletKey;
 use clap::{Args, ValueEnum};
 use lightning::offers::offer::Offer;
 
-use crate::utils::{get_number_input, get_user_input};
+use crate::utils::{get_number_input, get_or_create_wallet, get_user_input};
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
 pub enum PaymentType {
@@ -23,7 +26,7 @@ pub enum PaymentType {
 
 #[derive(Args)]
 pub struct MeltSubCommand {
-    /// Mpp
+    /// Use Multi-Path Payment (split payment across multiple mints, BOLT11 only)
     #[arg(short, long, conflicts_with = "mint_url")]
     mpp: bool,
     /// Mint URL to use for melting
@@ -78,42 +81,44 @@ fn input_or_prompt(arg: Option<&String>, prompt: &str) -> Result<String> {
 }
 
 pub async fn pay(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &MeltSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
-    // Check total balance across all wallets
-    let total_balance = multi_mint_wallet.total_balance().await?;
+    // Check total balance for the requested unit
+    let balances_by_unit = wallet_repository.total_balance().await?;
+    let total_balance = balances_by_unit.get(unit).copied().unwrap_or(Amount::ZERO);
     if total_balance == Amount::ZERO {
-        bail!("No funds available");
+        bail!("No funds available for unit {}", unit);
     }
 
-    // Determine which mint to use for melting BEFORE processing payment (unless using MPP)
-    let selected_mint = if sub_command_args.mpp {
-        None // MPP mode handles mint selection differently
-    } else if let Some(mint_url) = &sub_command_args.mint_url {
+    // Handle MPP mode separately
+    if sub_command_args.mpp {
+        return pay_mpp(wallet_repository, sub_command_args, unit).await;
+    }
+
+    // Determine which mint to use for melting
+    let selected_mint = if let Some(mint_url) = &sub_command_args.mint_url {
         Some(MintUrl::from_str(mint_url)?)
     } else {
         // Display all mints with their balances and let user select
-        let balances_map = multi_mint_wallet.get_balances().await?;
+        let balances_map = wallet_repository.get_balances().await?;
         if balances_map.is_empty() {
             bail!("No mints available in the wallet");
         }
 
-        let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect();
+        let balances_vec: Vec<(WalletKey, Amount)> = balances_map.into_iter().collect();
 
         // If only one mint exists, automatically select it
         if balances_vec.len() == 1 {
-            Some(balances_vec[0].0.clone())
+            Some(balances_vec[0].0.mint_url.clone())
         } else {
             // Display all mints with their balances and let user select
             println!("\nAvailable mints and balances:");
-            for (index, (mint_url, balance)) in balances_vec.iter().enumerate() {
+            for (index, (key, balance)) in balances_vec.iter().enumerate() {
                 println!(
-                    "  {}: {} - {} {}",
-                    index,
-                    mint_url,
-                    balance,
-                    multi_mint_wallet.unit()
+                    "  {}: {} ({}) - {} {}",
+                    index, key.mint_url, key.unit, balance, unit
                 );
             }
             println!("  {}: Any mint (auto-select best)", balances_vec.len());
@@ -126,8 +131,8 @@ pub async fn pay(
                     break None; // "Any" option selected
                 }
 
-                if let Some((mint_url, _)) = balances_vec.get(selection) {
-                    break Some(mint_url.clone());
+                if let Some((key, _)) = balances_vec.get(selection) {
+                    break Some(key.mint_url.clone());
                 }
 
                 println!("Invalid selection, please try again.");
@@ -137,293 +142,307 @@ pub async fn pay(
         }
     };
 
-    if sub_command_args.mpp {
-        // Manual MPP - user specifies which mints and amounts to use
-        if !matches!(sub_command_args.method, PaymentType::Bolt11) {
-            bail!("MPP is only supported for BOLT11 invoices");
-        }
+    let available_funds = <cdk::Amount as Into<u64>>::into(total_balance) * MSAT_IN_SAT;
 
-        let bolt11_str =
-            input_or_prompt(sub_command_args.invoice.as_ref(), "Enter bolt11 invoice")?;
-        let _bolt11 = Bolt11Invoice::from_str(&bolt11_str)?; // Validate invoice format
+    // Process payment based on payment method using individual wallets
+    match sub_command_args.method {
+        PaymentType::Bolt11 => {
+            // Process BOLT11 payment
+            let bolt11_str =
+                input_or_prompt(sub_command_args.invoice.as_ref(), "Enter bolt11 invoice")?;
+            let bolt11 = Bolt11Invoice::from_str(&bolt11_str)?;
 
-        // Show available mints and balances
-        let balances = multi_mint_wallet.get_balances().await?;
-        println!("\nAvailable mints and balances:");
-        for (i, (mint_url, balance)) in balances.iter().enumerate() {
-            println!(
-                "  {}: {} - {} {}",
-                i,
-                mint_url,
-                balance,
-                multi_mint_wallet.unit()
+            // Determine payment amount and options
+            let prompt = format!(
+                "Enter the amount you would like to pay in {} for this amountless invoice.",
+                unit
             );
-        }
+            let options =
+                create_melt_options(available_funds, bolt11.amount_milli_satoshis(), &prompt)?;
+
+            // Get or select a mint with sufficient balance
+            let mint_url = if let Some(specific_mint) = selected_mint {
+                specific_mint
+            } else {
+                // Auto-select the first mint with sufficient balance
+                let balances = wallet_repository.get_balances().await?;
+                let required_amount = bolt11
+                    .amount_milli_satoshis()
+                    .map(|a| Amount::from(a / MSAT_IN_SAT))
+                    .unwrap_or(Amount::ZERO);
+
+                balances
+                    .into_iter()
+                    .find(|(_, balance)| *balance >= required_amount)
+                    .map(|(key, _)| key.mint_url)
+                    .ok_or_else(|| anyhow::anyhow!("No mint with sufficient balance"))?
+            };
 
-        // Collect mint selections and amounts
-        let mut mint_amounts = Vec::new();
-        loop {
-            let mint_input = get_user_input("Enter mint number to use (or 'done' to finish)")?;
+            let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
+
+            // Get melt quote
+            let quote = wallet
+                .melt_quote(
+                    PaymentMethod::Known(KnownMethod::Bolt11),
+                    bolt11_str.clone(),
+                    options,
+                    None,
+                )
+                .await?;
+
+            println!("Melt quote created:");
+            println!("  Quote ID: {}", quote.id);
+            println!("  Amount: {}", quote.amount);
+            println!("  Fee Reserve: {}", quote.fee_reserve);
+
+            // Execute the melt
+            let melted = wallet
+                .prepare_melt(&quote.id, HashMap::new())
+                .await?
+                .confirm()
+                .await?;
 
-            if mint_input.to_lowercase() == "done" || mint_input.is_empty() {
-                break;
+            println!(
+                "Payment successful: state={}, amount={}, fee_paid={}",
+                melted.state(),
+                melted.amount(),
+                melted.fee_paid()
+            );
+            if let Some(preimage) = melted.payment_proof() {
+                println!("Payment preimage: {}", preimage);
             }
-
-            let mint_index: usize = mint_input.parse()?;
-            let mint_url = balances
-                .iter()
-                .nth(mint_index)
-                .map(|(url, _)| url.clone())
-                .ok_or_else(|| anyhow::anyhow!("Invalid mint index"))?;
-
-            let amount: u64 = get_number_input(&format!(
-                "Enter amount to use from this mint ({})",
-                multi_mint_wallet.unit()
-            ))?;
-            mint_amounts.push((mint_url, Amount::from(amount)));
-        }
-
-        if mint_amounts.is_empty() {
-            bail!("No mints selected for MPP payment");
         }
+        PaymentType::Bolt12 => {
+            // Process BOLT12 payment (offer)
+            let offer_str = input_or_prompt(sub_command_args.offer.as_ref(), "Enter BOLT12 offer")?;
+            let offer = Offer::from_str(&offer_str)
+                .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
+
+            // Determine if offer has an amount
+            let prompt = format!(
+                "Enter the amount you would like to pay in {} for this amountless offer:",
+                unit
+            );
+            let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
+                Ok(amount) => Some(u64::from(amount)),
+                Err(_) => None,
+            };
 
-        // Get quotes for each mint
-        println!("\nGetting melt quotes...");
-        let quotes = multi_mint_wallet
-            .mpp_melt_quote(bolt11_str, mint_amounts)
-            .await?;
+            let options = create_melt_options(available_funds, amount_msat, &prompt)?;
 
-        // Display quotes
-        println!("\nMelt quotes obtained:");
-        for (mint_url, quote) in &quotes {
-            println!("  {} - Quote ID: {}", mint_url, quote.id);
-            println!("    Amount: {}, Fee: {}", quote.amount, quote.fee_reserve);
-        }
+            // Get wallet for BOLT12 using the selected mint
+            let mint_url = if let Some(specific_mint) = selected_mint {
+                specific_mint
+            } else {
+                // User selected "Any" - just pick the first mint with any balance
+                let balances = wallet_repository.get_balances().await?;
 
-        // Execute the melts
-        let quotes_to_execute: Vec<(MintUrl, String)> = quotes
-            .iter()
-            .map(|(url, quote)| (url.clone(), quote.id.clone()))
-            .collect();
+                balances
+                    .into_iter()
+                    .find(|(_, balance)| *balance > Amount::ZERO)
+                    .map(|(key, _)| key.mint_url)
+                    .ok_or_else(|| anyhow::anyhow!("No mint available for BOLT12 payment"))?
+            };
 
-        println!("\nExecuting MPP payment...");
-        let results = multi_mint_wallet.mpp_melt(quotes_to_execute).await?;
+            let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
+
+            // Get melt quote for BOLT12
+            let quote = wallet
+                .melt_quote(
+                    PaymentMethod::Known(KnownMethod::Bolt12),
+                    offer_str,
+                    options,
+                    None,
+                )
+                .await?;
+
+            // Display quote info
+            println!("Melt quote created:");
+            println!("  Quote ID: {}", quote.id);
+            println!("  Amount: {}", quote.amount);
+            println!("  Fee Reserve: {}", quote.fee_reserve);
+            println!("  State: {}", quote.state);
+            println!("  Expiry: {}", quote.expiry);
+
+            // Execute the melt
+            let melted = wallet
+                .prepare_melt(&quote.id, HashMap::new())
+                .await?
+                .confirm()
+                .await?;
+            println!(
+                "Payment successful: Paid {} with fee {}",
+                melted.amount(),
+                melted.fee_paid()
+            );
+            if let Some(preimage) = melted.payment_proof() {
+                println!("Payment preimage: {}", preimage);
+            }
+        }
+        PaymentType::Bip353 => {
+            let bip353_addr =
+                input_or_prompt(sub_command_args.address.as_ref(), "Enter Bip353 address")?;
 
-        // Display results
-        println!("\nPayment results:");
-        let mut total_paid = Amount::ZERO;
-        let mut total_fees = Amount::ZERO;
+            let prompt = format!(
+                "Enter the amount you would like to pay in {} for this amountless offer:",
+                unit
+            );
+            // BIP353 payments are always amountless for now
+            let options = create_melt_options(available_funds, None, &prompt)?;
+
+            // Get wallet for BIP353 using the selected mint
+            let mint_url = if let Some(specific_mint) = selected_mint {
+                specific_mint
+            } else {
+                // User selected "Any" - just pick the first mint with any balance
+                let balances = wallet_repository.get_balances().await?;
+
+                balances
+                    .into_iter()
+                    .find(|(_, balance)| *balance > Amount::ZERO)
+                    .map(|(key, _)| key.mint_url)
+                    .ok_or_else(|| anyhow::anyhow!("No mint available for BIP353 payment"))?
+            };
 
-        for (mint_url, melted) in results {
+            let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
+
+            // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
+            let quote = wallet
+                .melt_bip353_quote(
+                    &bip353_addr,
+                    options.expect("Amount is required").amount_msat(),
+                )
+                .await?;
+
+            // Display quote info
+            println!("Melt quote created:");
+            println!("  Quote ID: {}", quote.id);
+            println!("  Amount: {}", quote.amount);
+            println!("  Fee Reserve: {}", quote.fee_reserve);
+            println!("  State: {}", quote.state);
+            println!("  Expiry: {}", quote.expiry);
+
+            // Execute the melt
+            let melted = wallet
+                .prepare_melt(&quote.id, HashMap::new())
+                .await?
+                .confirm()
+                .await?;
             println!(
-                "  {} - Paid: {}, Fee: {}",
-                mint_url,
+                "Payment successful: Paid {} with fee {}",
                 melted.amount(),
                 melted.fee_paid()
             );
-            total_paid += melted.amount();
-            total_fees += melted.fee_paid();
-
             if let Some(preimage) = melted.payment_proof() {
-                println!("    Preimage: {}", preimage);
+                println!("Payment preimage: {}", preimage);
             }
         }
+    }
 
-        println!("\nTotal paid: {} {}", total_paid, multi_mint_wallet.unit());
-        println!("Total fees: {} {}", total_fees, multi_mint_wallet.unit());
-    } else {
-        let available_funds = <cdk::Amount as Into<u64>>::into(total_balance) * MSAT_IN_SAT;
-
-        // Process payment based on payment method using new unified interface
-        match sub_command_args.method {
-            PaymentType::Bolt11 => {
-                // Process BOLT11 payment
-                let bolt11_str =
-                    input_or_prompt(sub_command_args.invoice.as_ref(), "Enter bolt11 invoice")?;
-                let bolt11 = Bolt11Invoice::from_str(&bolt11_str)?;
-
-                // Determine payment amount and options
-                let prompt = format!(
-                    "Enter the amount you would like to pay in {} for this amountless invoice.",
-                    multi_mint_wallet.unit()
-                );
-                let options =
-                    create_melt_options(available_funds, bolt11.amount_milli_satoshis(), &prompt)?;
-
-                // Use selected mint or auto-select
-                let melted = if let Some(mint_url) = selected_mint {
-                    // User selected a specific mint - use the new mint-specific functions
-                    let quote = multi_mint_wallet
-                        .melt_quote(
-                            &mint_url,
-                            PaymentMethod::BOLT11,
-                            bolt11_str.clone(),
-                            options,
-                            None,
-                        )
-                        .await?;
-
-                    println!("Melt quote created:");
-                    println!("  Quote ID: {}", quote.id);
-                    println!("  Amount: {}", quote.amount);
-                    println!("  Fee Reserve: {}", quote.fee_reserve);
-
-                    // Execute the melt
-                    multi_mint_wallet
-                        .melt_with_mint(&mint_url, &quote.id)
-                        .await?
-                } else {
-                    // User selected "Any" - let the wallet auto-select the best mint
-                    multi_mint_wallet.melt(&bolt11_str, options, None).await?
-                };
+    Ok(())
+}
 
-                println!(
-                    "Payment successful: state={}, amount={}, fee_paid={}",
-                    melted.state(),
-                    melted.amount(),
-                    melted.fee_paid()
-                );
-                if let Some(preimage) = melted.payment_proof() {
-                    println!("Payment preimage: {}", preimage);
-                }
-            }
-            PaymentType::Bolt12 => {
-                // Process BOLT12 payment (offer)
-                let offer_str =
-                    input_or_prompt(sub_command_args.offer.as_ref(), "Enter BOLT12 offer")?;
-                let offer = Offer::from_str(&offer_str)
-                    .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
-
-                // Determine if offer has an amount
-                let prompt = format!(
-                    "Enter the amount you would like to pay in {} for this amountless offer:",
-                    multi_mint_wallet.unit()
-                );
-                let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
-                    Ok(amount) => Some(u64::from(amount)),
-                    Err(_) => None,
-                };
-
-                let options = create_melt_options(available_funds, amount_msat, &prompt)?;
-
-                // Get wallet for BOLT12 using the selected mint
-                let mint_url = if let Some(specific_mint) = selected_mint {
-                    specific_mint
-                } else {
-                    // User selected "Any" - just pick the first mint with any balance
-                    let balances = multi_mint_wallet.get_balances().await?;
-
-                    balances
-                        .into_iter()
-                        .find(|(_, balance)| *balance > Amount::ZERO)
-                        .map(|(mint_url, _)| mint_url)
-                        .ok_or_else(|| anyhow::anyhow!("No mint available for BOLT12 payment"))?
-                };
-
-                let wallet = multi_mint_wallet
-                    .get_wallet(&mint_url)
-                    .await
-                    .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?;
-
-                // Get melt quote for BOLT12
-                let quote = wallet
-                    .melt_quote(PaymentMethod::BOLT12, offer_str, options, None)
-                    .await?;
-
-                // Display quote info
-                println!("Melt quote created:");
-                println!("  Quote ID: {}", quote.id);
-                println!("  Amount: {}", quote.amount);
-                println!("  Fee Reserve: {}", quote.fee_reserve);
-                println!("  State: {}", quote.state);
-                println!("  Expiry: {}", quote.expiry);
-
-                // Execute the melt
-                let prepared = wallet
-                    .prepare_melt(&quote.id, std::collections::HashMap::new())
-                    .await?;
-                println!(
-                    "Prepared melt - Amount: {}, Fee: {}",
-                    prepared.amount(),
-                    prepared.total_fee()
-                );
-                let confirmed = prepared.confirm().await?;
-                println!(
-                    "Payment successful: Paid {} with fee {}",
-                    confirmed.amount(),
-                    confirmed.fee_paid()
-                );
-                if let Some(preimage) = confirmed.payment_proof() {
-                    println!("Payment preimage: {}", preimage);
-                }
-            }
-            PaymentType::Bip353 => {
-                let bip353_addr =
-                    input_or_prompt(sub_command_args.address.as_ref(), "Enter Bip353 address")?;
+/// Handle Multi-Path Payment (MPP) - split a BOLT11 payment across multiple mints
+async fn pay_mpp(
+    wallet_repository: &WalletRepository,
+    sub_command_args: &MeltSubCommand,
+    unit: &CurrencyUnit,
+) -> Result<()> {
+    if !matches!(sub_command_args.method, PaymentType::Bolt11) {
+        bail!("MPP is only supported for BOLT11 invoices");
+    }
 
-                let prompt = format!(
-                    "Enter the amount you would like to pay in {} for this amountless offer:",
-                    multi_mint_wallet.unit()
-                );
-                // BIP353 payments are always amountless for now
-                let options = create_melt_options(available_funds, None, &prompt)?;
-
-                // Get wallet for BIP353 using the selected mint
-                let mint_url = if let Some(specific_mint) = selected_mint {
-                    specific_mint
-                } else {
-                    // User selected "Any" - just pick the first mint with any balance
-                    let balances = multi_mint_wallet.get_balances().await?;
-
-                    balances
-                        .into_iter()
-                        .find(|(_, balance)| *balance > Amount::ZERO)
-                        .map(|(mint_url, _)| mint_url)
-                        .ok_or_else(|| anyhow::anyhow!("No mint available for BIP353 payment"))?
-                };
-
-                let wallet = multi_mint_wallet
-                    .get_wallet(&mint_url)
-                    .await
-                    .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?;
-
-                // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
-                let quote = wallet
-                    .melt_bip353_quote(
-                        &bip353_addr,
-                        options.expect("Amount is required").amount_msat(),
-                    )
-                    .await?;
-
-                // Display quote info
-                println!("Melt quote created:");
-                println!("  Quote ID: {}", quote.id);
-                println!("  Amount: {}", quote.amount);
-                println!("  Fee Reserve: {}", quote.fee_reserve);
-                println!("  State: {}", quote.state);
-                println!("  Expiry: {}", quote.expiry);
-
-                // Execute the melt
-                let prepared = wallet
-                    .prepare_melt(&quote.id, std::collections::HashMap::new())
-                    .await?;
-                println!(
-                    "Prepared melt - Amount: {}, Fee: {}",
-                    prepared.amount(),
-                    prepared.total_fee()
-                );
-                let confirmed = prepared.confirm().await?;
-                println!(
-                    "Payment successful: Paid {} with fee {}",
-                    confirmed.amount(),
-                    confirmed.fee_paid()
-                );
-                if let Some(preimage) = confirmed.payment_proof() {
-                    println!("Payment preimage: {}", preimage);
-                }
-            }
+    let bolt11_str = input_or_prompt(sub_command_args.invoice.as_ref(), "Enter bolt11 invoice")?;
+    // Validate invoice format
+    let _bolt11 = Bolt11Invoice::from_str(&bolt11_str)?;
+
+    // Show available mints and balances
+    let balances = wallet_repository.get_balances().await?;
+    let balances_vec: Vec<(WalletKey, Amount)> = balances.into_iter().collect();
+
+    println!("\nAvailable mints and balances:");
+    for (i, (key, balance)) in balances_vec.iter().enumerate() {
+        println!(
+            "  {}: {} ({}) - {} {}",
+            i, key.mint_url, key.unit, balance, unit
+        );
+    }
+
+    // Collect mint selections and amounts from user
+    let mut mint_amounts: Vec<(MintUrl, Amount)> = Vec::new();
+    loop {
+        let mint_input = get_user_input("Enter mint number to use (or 'done' to finish)")?;
+
+        if mint_input.to_lowercase() == "done" || mint_input.is_empty() {
+            break;
         }
+
+        let mint_index: usize = mint_input.parse()?;
+        let (key, _) = balances_vec
+            .get(mint_index)
+            .ok_or_else(|| anyhow::anyhow!("Invalid mint index"))?;
+
+        let amount: u64 =
+            get_number_input(&format!("Enter amount to use from this mint ({})", unit))?;
+        mint_amounts.push((key.mint_url.clone(), Amount::from(amount)));
+    }
+
+    if mint_amounts.is_empty() {
+        bail!("No mints selected for MPP payment");
     }
 
+    // Get quotes from each mint with MPP options
+    println!("\nGetting melt quotes...");
+    let mut quotes = Vec::new();
+    for (mint_url, amount) in &mint_amounts {
+        let wallet = get_or_create_wallet(wallet_repository, mint_url, unit).await?;
+
+        // Convert amount to millisats for MPP
+        let amount_msat = u64::from(*amount) * MSAT_IN_SAT;
+        let options = Some(MeltOptions::new_mpp(amount_msat));
+
+        let quote = wallet
+            .melt_quote(
+                PaymentMethod::Known(KnownMethod::Bolt11),
+                bolt11_str.clone(),
+                options,
+                None,
+            )
+            .await?;
+
+        println!("  {} - Quote ID: {}", mint_url, quote.id);
+        println!("    Amount: {}, Fee: {}", quote.amount, quote.fee_reserve);
+        quotes.push((mint_url.clone(), wallet, quote));
+    }
+
+    // Execute all melts
+    println!("\nExecuting MPP payment...");
+    let mut total_paid = Amount::ZERO;
+    let mut total_fees = Amount::ZERO;
+
+    for (mint_url, wallet, quote) in quotes {
+        let melted = wallet
+            .prepare_melt(&quote.id, HashMap::new())
+            .await?
+            .confirm()
+            .await?;
+
+        println!(
+            "  {} - Paid: {}, Fee: {}",
+            mint_url,
+            melted.amount(),
+            melted.fee_paid()
+        );
+        total_paid += melted.amount();
+        total_fees += melted.fee_paid();
+
+        if let Some(preimage) = melted.payment_proof() {
+            println!("    Preimage: {}", preimage);
+        }
+    }
+
+    println!("\nTotal paid: {} {}", total_paid, unit);
+    println!("Total fees: {} {}", total_fees, unit);
+
     Ok(())
 }

+ 5 - 4
crates/cdk-cli/src/sub_commands/mint.rs

@@ -4,8 +4,8 @@ use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::PaymentMethod;
-use cdk::wallet::MultiMintWallet;
+use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::wallet::WalletRepository;
 use cdk::{Amount, StreamExt};
 use cdk_common::nut00::KnownMethod;
 use clap::Args;
@@ -40,13 +40,14 @@ pub struct MintSubCommand {
 }
 
 pub async fn mint(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &MintSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
     let description: Option<String> = sub_command_args.description.clone();
 
-    let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url).await?;
+    let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
 
     let payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
 

+ 45 - 52
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs

@@ -2,13 +2,14 @@ use std::path::Path;
 
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::MintInfo;
-use cdk::wallet::MultiMintWallet;
+use cdk::nuts::{CurrencyUnit, MintInfo};
+use cdk::wallet::WalletRepository;
 use cdk::{Amount, OidcClient};
 use clap::Args;
 use serde::{Deserialize, Serialize};
 
 use crate::token_storage;
+use crate::utils::get_or_create_wallet;
 
 #[derive(Args, Serialize, Deserialize)]
 pub struct MintBlindAuthSubCommand {
@@ -22,18 +23,22 @@ pub struct MintBlindAuthSubCommand {
 }
 
 pub async fn mint_blind_auth(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &MintBlindAuthSubCommand,
     work_dir: &Path,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
     // Ensure the mint exists
-    if !multi_mint_wallet.has_mint(&mint_url).await {
-        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    if !wallet_repository.has_mint(&mint_url).await {
+        wallet_repository.add_wallet(mint_url.clone()).await?;
     }
 
-    multi_mint_wallet.fetch_mint_info(&mint_url).await?;
+    wallet_repository.fetch_mint_info(&mint_url).await?;
+
+    // Get a wallet for this mint
+    let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
 
     // Try to get the token from the provided argument or from the stored file
     let cat = match &sub_command_args.cat {
@@ -61,7 +66,7 @@ pub async fn mint_blind_auth(
     };
 
     // Try to set the access token
-    if let Err(err) = multi_mint_wallet.set_cat(&mint_url, cat.clone()).await {
+    if let Err(err) = wallet.set_cat(cat.clone()).await {
         tracing::error!("Could not set cat: {}", err);
 
         // Try to refresh the token if we have a refresh token
@@ -69,42 +74,37 @@ pub async fn mint_blind_auth(
             println!("Attempting to refresh the access token...");
 
             // Get the mint info to access OIDC configuration
-            if let Some(mint_info) = multi_mint_wallet.fetch_mint_info(&mint_url).await? {
-                match refresh_access_token(&mint_info, &token_data.refresh_token).await {
-                    Ok((new_access_token, new_refresh_token)) => {
-                        println!("Successfully refreshed access token");
-
-                        // Save the new tokens
-                        if let Err(e) = token_storage::save_tokens(
-                            work_dir,
-                            &mint_url,
-                            &new_access_token,
-                            &new_refresh_token,
-                        )
-                        .await
-                        {
-                            println!("Warning: Failed to save refreshed tokens: {e}");
-                        }
-
-                        // Try setting the new access token
-                        if let Err(err) =
-                            multi_mint_wallet.set_cat(&mint_url, new_access_token).await
-                        {
-                            tracing::error!("Could not set refreshed cat: {}", err);
-                            return Err(anyhow::anyhow!(
-                                "Authentication failed even after token refresh"
-                            ));
-                        }
-
-                        // Set the refresh token
-                        multi_mint_wallet
-                            .set_refresh_token(&mint_url, new_refresh_token)
-                            .await?;
+            let mint_info = wallet_repository.fetch_mint_info(&mint_url).await?;
+            match refresh_access_token(&mint_info, &token_data.refresh_token).await {
+                Ok((new_access_token, new_refresh_token)) => {
+                    println!("Successfully refreshed access token");
+
+                    // Save the new tokens
+                    if let Err(e) = token_storage::save_tokens(
+                        work_dir,
+                        &mint_url,
+                        &new_access_token,
+                        &new_refresh_token,
+                    )
+                    .await
+                    {
+                        println!("Warning: Failed to save refreshed tokens: {e}");
                     }
-                    Err(e) => {
-                        tracing::error!("Failed to refresh token: {}", e);
-                        return Err(anyhow::anyhow!("Failed to refresh access token: {}", e));
+
+                    // Try setting the new access token
+                    if let Err(err) = wallet.set_cat(new_access_token).await {
+                        tracing::error!("Could not set refreshed cat: {}", err);
+                        return Err(anyhow::anyhow!(
+                            "Authentication failed even after token refresh"
+                        ));
                     }
+
+                    // Set the refresh token
+                    wallet.set_refresh_token(new_refresh_token).await?;
+                }
+                Err(e) => {
+                    tracing::error!("Failed to refresh token: {}", e);
+                    return Err(anyhow::anyhow!("Failed to refresh access token: {}", e));
                 }
             }
         } else {
@@ -116,10 +116,8 @@ pub async fn mint_blind_auth(
         // If we have a refresh token, set it
         if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
             tracing::info!("Attempting to use refresh access token to refresh auth token");
-            multi_mint_wallet
-                .set_refresh_token(&mint_url, token_data.refresh_token)
-                .await?;
-            multi_mint_wallet.refresh_access_token(&mint_url).await?;
+            wallet.set_refresh_token(token_data.refresh_token).await?;
+            wallet.refresh_access_token().await?;
         }
     }
 
@@ -128,19 +126,14 @@ pub async fn mint_blind_auth(
     let amount = match sub_command_args.amount {
         Some(amount) => amount,
         None => {
-            let mint_info = multi_mint_wallet
-                .fetch_mint_info(&mint_url)
-                .await?
-                .ok_or(anyhow!("Unknown mint info"))?;
+            let mint_info = wallet_repository.fetch_mint_info(&mint_url).await?;
             mint_info
                 .bat_max_mint()
                 .ok_or(anyhow!("Unknown max bat mint"))?
         }
     };
 
-    let proofs = multi_mint_wallet
-        .mint_blind_auth(&mint_url, Amount::from(amount))
-        .await?;
+    let proofs = wallet.mint_blind_auth(Amount::from(amount)).await?;
 
     println!("Received {} auth proofs for mint {mint_url}", proofs.len());
 

+ 38 - 30
crates/cdk-cli/src/sub_commands/npubcash.rs

@@ -6,28 +6,36 @@ use anyhow::{bail, Result};
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::wallet::{MultiMintWallet, Wallet};
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::{Wallet, WalletRepository};
 use cdk::StreamExt;
 use clap::Subcommand;
 use nostr_sdk::ToBech32;
 
 /// Helper function to get wallet for a specific mint URL
 async fn get_wallet_for_mint(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url_str: &str,
 ) -> Result<Arc<Wallet>> {
     let mint_url = MintUrl::from_str(mint_url_str)?;
 
     // Check if wallet exists for this mint
-    if !multi_mint_wallet.has_mint(&mint_url).await {
+    if !wallet_repository.has_mint(&mint_url).await {
         // Add the mint to the wallet
-        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+        wallet_repository.add_wallet(mint_url.clone()).await?;
     }
 
-    multi_mint_wallet
-        .get_wallet(&mint_url)
+    match wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
         .await
-        .ok_or_else(|| anyhow::anyhow!("Failed to get wallet for mint: {}", mint_url_str))
+    {
+        Ok(wallet) => Ok(Arc::new(wallet)),
+        Err(_) => Ok(Arc::new(
+            wallet_repository
+                .create_wallet(mint_url, CurrencyUnit::Sat, None)
+                .await?,
+        )),
+    }
 }
 
 #[derive(Subcommand)]
@@ -55,7 +63,7 @@ pub enum NpubCashSubCommand {
 }
 
 pub async fn npubcash(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url: &str,
     sub_command: &NpubCashSubCommand,
     npubcash_url: Option<String>,
@@ -64,23 +72,23 @@ pub async fn npubcash(
     let base_url = npubcash_url.unwrap_or_else(|| "https://npubx.cash".to_string());
 
     match sub_command {
-        NpubCashSubCommand::Sync => sync(multi_mint_wallet, mint_url, &base_url).await,
+        NpubCashSubCommand::Sync => sync(wallet_repository, mint_url, &base_url).await,
         NpubCashSubCommand::List { since, format } => {
-            list(multi_mint_wallet, mint_url, &base_url, *since, format).await
+            list(wallet_repository, mint_url, &base_url, *since, format).await
         }
-        NpubCashSubCommand::Subscribe => subscribe(multi_mint_wallet, mint_url, &base_url).await,
+        NpubCashSubCommand::Subscribe => subscribe(wallet_repository, mint_url, &base_url).await,
         NpubCashSubCommand::SetMint { url } => {
-            set_mint(multi_mint_wallet, mint_url, &base_url, url).await
+            set_mint(wallet_repository, mint_url, &base_url, url).await
         }
-        NpubCashSubCommand::ShowKeys => show_keys(multi_mint_wallet, mint_url).await,
+        NpubCashSubCommand::ShowKeys => show_keys(wallet_repository, mint_url).await,
     }
 }
 
 /// Helper function to ensure active mint consistency
-async fn ensure_active_mint(multi_mint_wallet: &MultiMintWallet, mint_url: &str) -> Result<()> {
+async fn ensure_active_mint(wallet_repository: &WalletRepository, mint_url: &str) -> Result<()> {
     let mint_url_struct = MintUrl::from_str(mint_url)?;
 
-    match multi_mint_wallet.get_active_npubcash_mint().await? {
+    match wallet_repository.get_active_npubcash_mint().await? {
         Some(active_mint) => {
             if active_mint != mint_url_struct {
                 bail!(
@@ -96,7 +104,7 @@ async fn ensure_active_mint(multi_mint_wallet: &MultiMintWallet, mint_url: &str)
         }
         None => {
             // No active mint set, set this one as active
-            multi_mint_wallet
+            wallet_repository
                 .set_active_npubcash_mint(mint_url_struct)
                 .await?;
             println!("✓ Set {} as active NpubCash mint", mint_url);
@@ -105,12 +113,12 @@ async fn ensure_active_mint(multi_mint_wallet: &MultiMintWallet, mint_url: &str)
     Ok(())
 }
 
-async fn sync(multi_mint_wallet: &MultiMintWallet, mint_url: &str, base_url: &str) -> Result<()> {
-    ensure_active_mint(multi_mint_wallet, mint_url).await?;
+async fn sync(wallet_repository: &WalletRepository, mint_url: &str, base_url: &str) -> Result<()> {
+    ensure_active_mint(wallet_repository, mint_url).await?;
 
     println!("Syncing quotes from NpubCash...");
 
-    let wallet = get_wallet_for_mint(multi_mint_wallet, mint_url).await?;
+    let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
 
     // Enable NpubCash if not already enabled
     wallet.enable_npubcash(base_url.to_string()).await?;
@@ -122,15 +130,15 @@ async fn sync(multi_mint_wallet: &MultiMintWallet, mint_url: &str, base_url: &st
 }
 
 async fn list(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url: &str,
     base_url: &str,
     since: Option<u64>,
     format: &str,
 ) -> Result<()> {
-    ensure_active_mint(multi_mint_wallet, mint_url).await?;
+    ensure_active_mint(wallet_repository, mint_url).await?;
 
-    let wallet = get_wallet_for_mint(multi_mint_wallet, mint_url).await?;
+    let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
 
     // Enable NpubCash if not already enabled
     wallet.enable_npubcash(base_url.to_string()).await?;
@@ -170,15 +178,15 @@ async fn list(
 }
 
 async fn subscribe(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url: &str,
     base_url: &str,
 ) -> Result<()> {
-    ensure_active_mint(multi_mint_wallet, mint_url).await?;
+    ensure_active_mint(wallet_repository, mint_url).await?;
 
     println!("=== NpubCash Quote Subscription ===\n");
 
-    let wallet = get_wallet_for_mint(multi_mint_wallet, mint_url).await?;
+    let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
 
     // Enable NpubCash if not already enabled
     wallet.enable_npubcash(base_url.to_string()).await?;
@@ -241,7 +249,7 @@ async fn subscribe(
 }
 
 async fn set_mint(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url: &str,
     base_url: &str,
     url: &str,
@@ -250,11 +258,11 @@ async fn set_mint(
 
     // Update active mint in KV store
     let mint_url_struct = MintUrl::from_str(mint_url)?;
-    multi_mint_wallet
+    wallet_repository
         .set_active_npubcash_mint(mint_url_struct)
         .await?;
 
-    let wallet = get_wallet_for_mint(multi_mint_wallet, mint_url).await?;
+    let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
 
     // Enable NpubCash if not already enabled
     wallet.enable_npubcash(base_url.to_string()).await?;
@@ -290,8 +298,8 @@ async fn set_mint(
     Ok(())
 }
 
-async fn show_keys(multi_mint_wallet: &MultiMintWallet, mint_url: &str) -> Result<()> {
-    let wallet = get_wallet_for_mint(multi_mint_wallet, mint_url).await?;
+async fn show_keys(wallet_repository: &WalletRepository, mint_url: &str) -> Result<()> {
+    let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
 
     let keys = wallet.get_npubcash_keys()?;
     let npub = keys.public_key().to_bech32()?;

+ 3 - 3
crates/cdk-cli/src/sub_commands/pay_request.rs

@@ -2,7 +2,7 @@ use std::io::{self, Write};
 
 use anyhow::{anyhow, Result};
 use cdk::nuts::PaymentRequest;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Amount;
 use clap::Args;
 
@@ -15,7 +15,7 @@ pub struct PayRequestSubCommand {
 }
 
 pub async fn pay_request(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &PayRequestSubCommand,
 ) -> Result<()> {
     let payment_request = &sub_command_args.payment_request;
@@ -43,7 +43,7 @@ pub async fn pay_request(
 
     let request_mints = &payment_request.mints;
 
-    let wallet_mints = multi_mint_wallet.get_wallets().await;
+    let wallet_mints = wallet_repository.get_wallets().await;
 
     // Wallets where unit, balance and mint match request
     let mut matching_wallets = vec![];

+ 11 - 4
crates/cdk-cli/src/sub_commands/pending_mints.rs

@@ -1,10 +1,17 @@
 use anyhow::Result;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
+use cdk::Amount;
 
-pub async fn mint_pending(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
-    let amount = multi_mint_wallet.mint_unissued_quotes(None).await?;
+pub async fn mint_pending(wallet_repository: &WalletRepository) -> Result<()> {
+    let wallets = wallet_repository.get_wallets().await;
+    let mut total_amount = Amount::ZERO;
 
-    println!("Amount: {amount}");
+    for wallet in wallets {
+        let amount = wallet.check_all_pending_proofs().await?;
+        total_amount += amount;
+    }
+
+    println!("Amount: {total_amount}");
 
     Ok(())
 }

+ 28 - 38
crates/cdk-cli/src/sub_commands/receive.rs

@@ -4,11 +4,9 @@ use std::str::FromStr;
 use std::time::Duration;
 
 use anyhow::{anyhow, Result};
-use cdk::mint_url::MintUrl;
-use cdk::nuts::{SecretKey, Token};
+use cdk::nuts::{CurrencyUnit, SecretKey, Token};
 use cdk::util::unix_time;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::{MultiMintReceiveOptions, ReceiveOptions};
+use cdk::wallet::{ReceiveOptions, WalletRepository};
 use cdk::Amount;
 use clap::Args;
 use nostr_sdk::nips::nip04;
@@ -39,15 +37,13 @@ pub struct ReceiveSubCommand {
     /// Allow receiving from untrusted mints (mints not already in the wallet)
     #[arg(long, default_value = "false")]
     allow_untrusted: bool,
-    /// Transfer tokens from untrusted mints to this mint
-    #[arg(long, value_name = "MINT_URL")]
-    transfer_to: Option<String>,
 }
 
 pub async fn receive(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &ReceiveSubCommand,
     work_dir: &Path,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     let mut signing_keys = Vec::new();
 
@@ -71,12 +67,12 @@ pub async fn receive(
     let amount = match &sub_command_args.token {
         Some(token_str) => {
             receive_token(
-                multi_mint_wallet,
+                wallet_repository,
                 token_str,
                 &signing_keys,
                 &sub_command_args.preimage,
                 sub_command_args.allow_untrusted,
-                sub_command_args.transfer_to.as_deref(),
+                unit,
             )
             .await?
         }
@@ -113,12 +109,12 @@ pub async fn receive(
             let mut total_amount = Amount::ZERO;
             for token_str in &tokens {
                 match receive_token(
-                    multi_mint_wallet,
+                    wallet_repository,
                     token_str,
                     &signing_keys,
                     &sub_command_args.preimage,
                     sub_command_args.allow_untrusted,
-                    sub_command_args.transfer_to.as_deref(),
+                    unit,
                 )
                 .await
                 {
@@ -141,46 +137,40 @@ pub async fn receive(
 }
 
 async fn receive_token(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     token_str: &str,
     signing_keys: &[SecretKey],
     preimage: &[String],
     allow_untrusted: bool,
-    transfer_to: Option<&str>,
+    unit: &CurrencyUnit,
 ) -> Result<Amount> {
     let token: Token = Token::from_str(token_str)?;
 
     let mint_url = token.mint_url()?;
 
-    // Parse transfer_to mint URL if provided
-    let transfer_to_mint = if let Some(mint_str) = transfer_to {
-        Some(MintUrl::from_str(mint_str)?)
-    } else {
-        None
-    };
-
     // Check if the mint is already trusted
-    let is_trusted = multi_mint_wallet.get_wallet(&mint_url).await.is_some();
+    let is_trusted = wallet_repository.has_mint(&mint_url).await;
 
-    // If mint is not trusted and we don't allow untrusted, add it first (old behavior)
+    // If mint is not trusted and we don't allow untrusted, error out
     if !is_trusted && !allow_untrusted {
-        get_or_create_wallet(multi_mint_wallet, &mint_url).await?;
+        return Err(anyhow!(
+            "Mint {} is not trusted. Use --allow-untrusted to receive from untrusted mints.",
+            mint_url
+        ));
     }
 
-    // Create multi-mint receive options
-    let multi_mint_options = MultiMintReceiveOptions::default()
-        .allow_untrusted(allow_untrusted)
-        .transfer_to_mint(transfer_to_mint)
-        .receive_options(ReceiveOptions {
-            p2pk_signing_keys: signing_keys.to_vec(),
-            preimages: preimage.to_vec(),
-            ..Default::default()
-        });
-
-    let amount = multi_mint_wallet
-        .receive(token_str, multi_mint_options)
-        .await?;
-    Ok(amount)
+    // Get or create wallet for the token's mint
+    let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
+
+    // Create receive options
+    let receive_options = ReceiveOptions {
+        p2pk_signing_keys: signing_keys.to_vec(),
+        preimages: preimage.to_vec(),
+        ..Default::default()
+    };
+
+    let received = wallet.receive(token_str, receive_options).await?;
+    Ok(received)
 }
 
 /// Receive tokens sent to nostr pubkey via dm

+ 7 - 13
crates/cdk-cli/src/sub_commands/restore.rs

@@ -1,8 +1,11 @@
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::wallet::MultiMintWallet;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::WalletRepository;
 use clap::Args;
 
+use crate::utils::get_or_create_wallet;
+
 #[derive(Args)]
 pub struct RestoreSubCommand {
     /// Mint Url
@@ -10,22 +13,13 @@ pub struct RestoreSubCommand {
 }
 
 pub async fn restore(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &RestoreSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
-    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
-        Some(wallet) => wallet.clone(),
-        None => {
-            multi_mint_wallet.add_mint(mint_url.clone()).await?;
-            multi_mint_wallet
-                .get_wallet(&mint_url)
-                .await
-                .expect("Wallet should exist after adding mint")
-                .clone()
-        }
-    };
+    let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
 
     let restored = wallet.restore().await?;
 

+ 32 - 88
crates/cdk-cli/src/sub_commands/send.rs

@@ -2,13 +2,13 @@ use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
+use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
 use cdk::wallet::types::SendKind;
-use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
+use cdk::wallet::{SendMemo, SendOptions, WalletRepository};
 use cdk::Amount;
 use clap::Args;
 
-use crate::utils::get_number_input;
+use crate::utils::{get_number_input, get_or_create_wallet};
 
 #[derive(Args)]
 pub struct SendSubCommand {
@@ -48,70 +48,50 @@ pub struct SendSubCommand {
     /// Mint URL to use for sending
     #[arg(long)]
     mint_url: Option<String>,
-    /// Allow transferring funds from other mints if the target mint has insufficient balance
-    #[arg(long)]
-    allow_transfer: bool,
-    /// Maximum amount to transfer from other mints
-    #[arg(long)]
-    max_transfer_amount: Option<u64>,
-
-    /// Specific mints to exclude from transfers (can be specified multiple times)
-    #[arg(long, action = clap::ArgAction::Append)]
-    excluded_mints: Vec<String>,
     /// Amount to send
     #[arg(short, long)]
     amount: Option<u64>,
 }
 
 pub async fn send(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &SendSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     // Determine which mint to use for sending BEFORE asking for amount
     let selected_mint = if let Some(mint_url) = &sub_command_args.mint_url {
-        Some(MintUrl::from_str(mint_url)?)
+        MintUrl::from_str(mint_url)?
     } else {
         // Get all mints with their balances
-        let balances_map = multi_mint_wallet.get_balances().await?;
+        let balances_map = wallet_repository.get_balances().await?;
         if balances_map.is_empty() {
             return Err(anyhow!("No mints available in the wallet"));
         }
 
-        let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect();
+        let balances_vec: Vec<_> = balances_map.into_iter().collect();
 
         // If only one mint exists, automatically select it
         if balances_vec.len() == 1 {
-            Some(balances_vec[0].0.clone())
+            balances_vec[0].0.mint_url.clone()
         } else {
             // Display all mints with their balances and let user select
             println!("\nAvailable mints and balances:");
-            for (index, (mint_url, balance)) in balances_vec.iter().enumerate() {
+            for (index, (key, balance)) in balances_vec.iter().enumerate() {
                 println!(
-                    "  {}: {} - {} {}",
-                    index,
-                    mint_url,
-                    balance,
-                    multi_mint_wallet.unit()
+                    "  {}: {} ({}) - {} {}",
+                    index, key.mint_url, key.unit, balance, unit
                 );
             }
-            println!("  {}: Any mint (auto-select best)", balances_vec.len());
 
-            let selection = loop {
-                let selection: usize =
-                    get_number_input("Enter mint number to send from (or select Any)")?;
+            loop {
+                let selection: usize = get_number_input("Enter mint number to send from")?;
 
-                if selection == balances_vec.len() {
-                    break None; // "Any" option selected
-                }
-
-                if let Some((mint_url, _)) = balances_vec.get(selection) {
-                    break Some(mint_url.clone());
+                if let Some((key, _)) = balances_vec.get(selection) {
+                    break key.mint_url.clone();
                 }
 
                 println!("Invalid selection, please try again.");
-            };
-
-            selection
+            }
         }
     };
 
@@ -119,16 +99,19 @@ pub async fn send(
         Some(amount) => Amount::from(amount),
         None => Amount::from(get_number_input::<u64>(&format!(
             "Enter value of token in {}",
-            multi_mint_wallet.unit()
+            unit
         ))?),
     };
 
-    // Check total balance across all wallets
-    let total_balance = multi_mint_wallet.total_balance().await?;
-    if total_balance < token_amount {
+    // Get or create wallet for the selected mint
+    let wallet = get_or_create_wallet(wallet_repository, &selected_mint, unit).await?;
+
+    // Check wallet balance
+    let balance = wallet.total_balance().await?;
+    if balance < token_amount {
         return Err(anyhow!(
-            "Insufficient funds. Total balance: {}, Required: {}",
-            total_balance,
+            "Insufficient funds. Wallet balance: {}, Required: {}",
+            balance,
             token_amount
         ));
     }
@@ -265,54 +248,15 @@ pub async fn send(
         ..Default::default()
     };
 
-    // Parse excluded mints from CLI arguments
-    let excluded_mints: Result<Vec<MintUrl>, _> = sub_command_args
-        .excluded_mints
-        .iter()
-        .map(|url| MintUrl::from_str(url))
-        .collect();
-    let excluded_mints = excluded_mints?;
-
-    // Send based on mint selection
-    let token = if let Some(specific_mint) = selected_mint {
-        // User selected a specific mint
-        let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions {
-            allow_transfer: sub_command_args.allow_transfer,
-            max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from),
-            allowed_mints: vec![specific_mint.clone()], // Use selected mint as the only allowed mint
-            excluded_mints,
-            send_options: send_options.clone(),
-        };
-
-        multi_mint_wallet
-            .send(specific_mint, token_amount, multi_mint_options)
-            .await?
-    } else {
-        // User selected "Any" - find the first mint with sufficient balance
-        let balances = multi_mint_wallet.get_balances().await?;
-        let best_mint = balances
-            .into_iter()
-            .find(|(_, balance)| *balance >= token_amount)
-            .map(|(mint_url, _)| mint_url)
-            .ok_or_else(|| anyhow!("No mint has sufficient balance for the requested amount"))?;
-
-        let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions {
-            allow_transfer: sub_command_args.allow_transfer,
-            max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from),
-            allowed_mints: vec![best_mint.clone()], // Use the best mint as the only allowed mint
-            excluded_mints,
-            send_options: send_options.clone(),
-        };
-
-        multi_mint_wallet
-            .send(best_mint, token_amount, multi_mint_options)
-            .await?
-    };
+    // Prepare and confirm the send using the individual wallet
+    let prepared = wallet
+        .prepare_send(token_amount, send_options.clone())
+        .await?;
+    let memo = send_options.memo;
+    let token = prepared.confirm(memo).await?;
 
     match sub_command_args.v3 {
         true => {
-            let token = token;
-
             println!("{}", token.to_v3_string());
         }
         false => {

+ 95 - 74
crates/cdk-cli/src/sub_commands/transfer.rs

@@ -2,9 +2,9 @@ use std::str::FromStr;
 
 use anyhow::{bail, Result};
 use cdk::mint_url::MintUrl;
-use cdk::wallet::multi_mint_wallet::TransferMode;
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::WalletRepository;
 use cdk::Amount;
+use cdk_common::wallet::WalletKey;
 use clap::Args;
 
 use crate::utils::get_number_input;
@@ -27,16 +27,17 @@ pub struct TransferSubCommand {
 
 /// Helper function to select a mint from available mints
 async fn select_mint(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     prompt: &str,
     exclude_mint: Option<&MintUrl>,
+    unit: &cdk::nuts::CurrencyUnit,
 ) -> Result<MintUrl> {
-    let balances = multi_mint_wallet.get_balances().await?;
+    let balances = wallet_repository.get_balances().await?;
 
     // Filter out excluded mint if provided
     let available_mints: Vec<_> = balances
         .iter()
-        .filter(|(url, _)| exclude_mint.is_none_or(|excluded| url != &excluded))
+        .filter(|(key, _)| exclude_mint.is_none_or(|excluded| &key.mint_url != excluded))
         .collect();
 
     if available_mints.is_empty() {
@@ -44,38 +45,37 @@ async fn select_mint(
     }
 
     println!("\nAvailable mints:");
-    for (i, (mint_url, balance)) in available_mints.iter().enumerate() {
+    for (i, (key, balance)) in available_mints.iter().enumerate() {
         println!(
-            "  {}: {} - {} {}",
-            i,
-            mint_url,
-            balance,
-            multi_mint_wallet.unit()
+            "  {}: {} ({}) - {} {}",
+            i, key.mint_url, key.unit, balance, unit
         );
     }
 
     let mint_number: usize = get_number_input(prompt)?;
     available_mints
         .get(mint_number)
-        .map(|(url, _)| (*url).clone())
+        .map(|(key, _)| key.mint_url.clone())
         .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))
 }
 
 pub async fn transfer(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &TransferSubCommand,
+    unit: &cdk::nuts::CurrencyUnit,
 ) -> Result<()> {
-    // Check total balance across all wallets
-    let total_balance = multi_mint_wallet.total_balance().await?;
+    // Check total balance for the requested unit
+    let balances_by_unit = wallet_repository.total_balance().await?;
+    let total_balance = balances_by_unit.get(unit).copied().unwrap_or(Amount::ZERO);
     if total_balance == Amount::ZERO {
-        bail!("No funds available");
+        bail!("No funds available for unit {}", unit);
     }
 
     // Get source mint URL either from args or by prompting user
     let source_mint_url = if let Some(source_mint) = &sub_command_args.source_mint {
         let url = MintUrl::from_str(source_mint)?;
         // Verify the mint is in the wallet
-        if !multi_mint_wallet.has_mint(&url).await {
+        if !wallet_repository.has_mint(&url).await {
             bail!(
                 "Source mint {} is not in the wallet. Please add it first.",
                 url
@@ -85,9 +85,10 @@ pub async fn transfer(
     } else {
         // Show available mints and let user select source
         select_mint(
-            multi_mint_wallet,
+            wallet_repository,
             "Enter source mint number to transfer from",
             None,
+            unit,
         )
         .await?
     };
@@ -96,7 +97,7 @@ pub async fn transfer(
     let target_mint_url = if let Some(target_mint) = &sub_command_args.target_mint {
         let url = MintUrl::from_str(target_mint)?;
         // Verify the mint is in the wallet
-        if !multi_mint_wallet.has_mint(&url).await {
+        if !wallet_repository.has_mint(&url).await {
             bail!(
                 "Target mint {} is not in the wallet. Please add it first.",
                 url
@@ -106,9 +107,10 @@ pub async fn transfer(
     } else {
         // Show available mints (excluding source) and let user select target
         select_mint(
-            multi_mint_wallet,
+            wallet_repository,
             "Enter target mint number to transfer to",
             Some(&source_mint_url),
+            unit,
         )
         .await?
     };
@@ -119,32 +121,61 @@ pub async fn transfer(
     }
 
     // Check source mint balance
-    let balances = multi_mint_wallet.get_balances().await?;
-    let source_balance = balances
-        .get(&source_mint_url)
-        .copied()
-        .unwrap_or(Amount::ZERO);
+    let balances = wallet_repository.get_balances().await?;
+    let source_key = WalletKey::new(source_mint_url.clone(), unit.clone());
+    let source_balance = balances.get(&source_key).copied().unwrap_or(Amount::ZERO);
 
     if source_balance == Amount::ZERO {
         bail!("Source mint has no balance to transfer");
     }
 
-    // Determine transfer mode based on user input
-    let transfer_mode = if sub_command_args.full_balance {
+    // Get source and target wallets
+    let source_wallet = wallet_repository.get_wallet(&source_mint_url, unit).await?;
+    let target_wallet = wallet_repository.get_wallet(&target_mint_url, unit).await?;
+
+    // Determine transfer mode and execute
+    if sub_command_args.full_balance {
         println!(
             "\nTransferring full balance ({} {}) from {} to {}...",
-            source_balance,
-            multi_mint_wallet.unit(),
-            source_mint_url,
-            target_mint_url
+            source_balance, unit, source_mint_url, target_mint_url
+        );
+
+        // Send all from source
+        let prepared = source_wallet
+            .prepare_send(source_balance, Default::default())
+            .await?;
+        let token = prepared.confirm(None).await?;
+
+        // Receive at target
+        let received = target_wallet
+            .receive(&token.to_string(), Default::default())
+            .await?;
+
+        let source_balance_after = source_wallet.total_balance().await?;
+        let target_balance_after = target_wallet.total_balance().await?;
+
+        println!("\nTransfer completed successfully!");
+        println!("Amount sent: {} {}", source_balance, unit);
+        println!("Amount received: {} {}", received, unit);
+        let fees_paid = source_balance - received;
+        if fees_paid > Amount::ZERO {
+            println!("Fees paid: {} {}", fees_paid, unit);
+        }
+        println!("\nUpdated balances:");
+        println!(
+            "  Source mint ({}): {} {}",
+            source_mint_url, source_balance_after, unit
+        );
+        println!(
+            "  Target mint ({}): {} {}",
+            target_mint_url, target_balance_after, unit
         );
-        TransferMode::FullBalance
     } else {
         let amount = match sub_command_args.amount {
             Some(amt) => Amount::from(amt),
             None => Amount::from(get_number_input::<u64>(&format!(
                 "Enter amount to transfer in {}",
-                multi_mint_wallet.unit()
+                unit
             ))?),
         };
 
@@ -152,58 +183,48 @@ pub async fn transfer(
             bail!(
                 "Insufficient funds in source mint. Available: {} {}, Required: {} {}",
                 source_balance,
-                multi_mint_wallet.unit(),
+                unit,
                 amount,
-                multi_mint_wallet.unit()
+                unit
             );
         }
 
         println!(
             "\nTransferring {} {} from {} to {}...",
-            amount,
-            multi_mint_wallet.unit(),
-            source_mint_url,
-            target_mint_url
+            amount, unit, source_mint_url, target_mint_url
         );
-        TransferMode::ExactReceive(amount)
-    };
 
-    // Perform the transfer
-    let transfer_result = multi_mint_wallet
-        .transfer(&source_mint_url, &target_mint_url, transfer_mode)
-        .await?;
-
-    println!("\nTransfer completed successfully!");
-    println!(
-        "Amount sent: {} {}",
-        transfer_result.amount_sent,
-        multi_mint_wallet.unit()
-    );
-    println!(
-        "Amount received: {} {}",
-        transfer_result.amount_received,
-        multi_mint_wallet.unit()
-    );
-    if transfer_result.fees_paid > Amount::ZERO {
+        // Send from source
+        let prepared = source_wallet
+            .prepare_send(amount, Default::default())
+            .await?;
+        let token = prepared.confirm(None).await?;
+
+        // Receive at target
+        let received = target_wallet
+            .receive(&token.to_string(), Default::default())
+            .await?;
+
+        let source_balance_after = source_wallet.total_balance().await?;
+        let target_balance_after = target_wallet.total_balance().await?;
+
+        println!("\nTransfer completed successfully!");
+        println!("Amount sent: {} {}", amount, unit);
+        println!("Amount received: {} {}", received, unit);
+        let fees_paid = amount - received;
+        if fees_paid > Amount::ZERO {
+            println!("Fees paid: {} {}", fees_paid, unit);
+        }
+        println!("\nUpdated balances:");
+        println!(
+            "  Source mint ({}): {} {}",
+            source_mint_url, source_balance_after, unit
+        );
         println!(
-            "Fees paid: {} {}",
-            transfer_result.fees_paid,
-            multi_mint_wallet.unit()
+            "  Target mint ({}): {} {}",
+            target_mint_url, target_balance_after, unit
         );
     }
-    println!("\nUpdated balances:");
-    println!(
-        "  Source mint ({}): {} {}",
-        source_mint_url,
-        transfer_result.source_balance_after,
-        multi_mint_wallet.unit()
-    );
-    println!(
-        "  Target mint ({}): {} {}",
-        target_mint_url,
-        transfer_result.target_balance_after,
-        multi_mint_wallet.unit()
-    );
 
     Ok(())
 }

+ 10 - 5
crates/cdk-cli/src/sub_commands/update_mint_url.rs

@@ -1,6 +1,7 @@
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::wallet::MultiMintWallet;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::WalletRepository;
 use clap::Args;
 
 #[derive(Args)]
@@ -12,17 +13,21 @@ pub struct UpdateMintUrlSubCommand {
 }
 
 pub async fn update_mint_url(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     sub_command_args: &UpdateMintUrlSubCommand,
+    unit: &CurrencyUnit,
 ) -> Result<()> {
     let UpdateMintUrlSubCommand {
         old_mint_url,
         new_mint_url,
     } = sub_command_args;
 
-    multi_mint_wallet
-        .update_mint_url(old_mint_url, new_mint_url.clone())
-        .await?;
+    let mut wallet = wallet_repository
+        .get_wallet(&sub_command_args.old_mint_url, unit)
+        .await?
+        .clone();
+
+    wallet.update_mint_url(new_mint_url.clone()).await?;
 
     println!("Mint Url changed from {old_mint_url} to {new_mint_url}");
 

+ 12 - 14
crates/cdk-cli/src/utils.rs

@@ -3,7 +3,8 @@ use std::str::FromStr;
 
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::WalletRepository;
 
 /// Helper function to get user input with a prompt
 pub fn get_user_input(prompt: &str) -> Result<String> {
@@ -25,20 +26,17 @@ where
     Ok(number)
 }
 
-/// Helper function to create or get a wallet
+/// Helper function to get an existing wallet or create one if it doesn't exist
 pub async fn get_or_create_wallet(
-    multi_mint_wallet: &MultiMintWallet,
+    wallet_repository: &WalletRepository,
     mint_url: &MintUrl,
-) -> Result<std::sync::Arc<cdk::wallet::Wallet>> {
-    match multi_mint_wallet.get_wallet(mint_url).await {
-        Some(wallet) => Ok(wallet),
-        None => {
-            tracing::debug!("Wallet does not exist creating..");
-            multi_mint_wallet.add_mint(mint_url.clone()).await?;
-            Ok(multi_mint_wallet
-                .get_wallet(mint_url)
-                .await
-                .expect("Wallet should exist after adding mint"))
-        }
+    unit: &CurrencyUnit,
+) -> Result<cdk::wallet::Wallet> {
+    match wallet_repository.get_wallet(mint_url, unit).await {
+        Ok(wallet) => Ok(wallet),
+        Err(_) => wallet_repository
+            .create_wallet(mint_url.clone(), unit.clone(), None)
+            .await
+            .map_err(Into::into),
     }
 }

+ 3 - 1
crates/cdk-common/src/wallet/mod.rs

@@ -278,7 +278,9 @@ impl MintQuote {
             }
         } else {
             // Other payment methods track incremental payments
-            self.amount_paid.saturating_sub(self.amount_issued)
+            self.amount_paid
+                .checked_sub(self.amount_issued)
+                .unwrap_or(Amount::ZERO)
         }
     }
 }

+ 2 - 2
crates/cdk-ffi/src/lib.rs

@@ -9,7 +9,6 @@
 pub mod database;
 pub mod error;
 pub mod logging;
-pub mod multi_mint_wallet;
 #[cfg(feature = "npubcash")]
 pub mod npubcash;
 #[cfg(feature = "postgres")]
@@ -18,15 +17,16 @@ pub mod sqlite;
 pub mod token;
 pub mod types;
 pub mod wallet;
+pub mod wallet_repository;
 
 pub use database::*;
 pub use error::*;
 pub use logging::*;
-pub use multi_mint_wallet::*;
 #[cfg(feature = "npubcash")]
 pub use npubcash::*;
 pub use types::*;
 pub use wallet::*;
+pub use wallet_repository::*;
 
 uniffi::setup_scaffolding!();
 

+ 0 - 1206
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -1,1206 +0,0 @@
-//! FFI MultiMintWallet bindings
-
-use std::collections::HashMap;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use bip39::Mnemonic;
-use cdk::wallet::multi_mint_wallet::{
-    MultiMintReceiveOptions as CdkMultiMintReceiveOptions,
-    MultiMintSendOptions as CdkMultiMintSendOptions, MultiMintWallet as CdkMultiMintWallet,
-    TokenData as CdkTokenData, TransferMode as CdkTransferMode,
-    TransferResult as CdkTransferResult,
-};
-
-use crate::error::FfiError;
-use crate::token::Token;
-use crate::types::payment_request::{
-    CreateRequestParams, CreateRequestResult, NostrWaitInfo, PaymentRequest,
-};
-use crate::types::*;
-
-/// FFI-compatible MultiMintWallet
-#[derive(uniffi::Object)]
-pub struct MultiMintWallet {
-    inner: Arc<CdkMultiMintWallet>,
-}
-
-#[uniffi::export(async_runtime = "tokio")]
-impl MultiMintWallet {
-    /// Create a new MultiMintWallet from mnemonic using WalletDatabaseFfi trait
-    #[uniffi::constructor]
-    pub fn new(
-        unit: CurrencyUnit,
-        mnemonic: String,
-        db: Arc<dyn crate::database::WalletDatabase>,
-    ) -> Result<Self, FfiError> {
-        // Parse mnemonic and generate seed without passphrase
-        let m = Mnemonic::parse(&mnemonic)
-            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
-        let seed = m.to_seed_normalized("");
-
-        // Convert the FFI database trait to a CDK database implementation
-        let localstore = crate::database::create_cdk_database_from_ffi(db);
-
-        let wallet = match tokio::runtime::Handle::try_current() {
-            Ok(handle) => tokio::task::block_in_place(|| {
-                handle.block_on(async move {
-                    CdkMultiMintWallet::new(localstore, seed, unit.into()).await
-                })
-            }),
-            Err(_) => {
-                // No current runtime, create a new one
-                tokio::runtime::Runtime::new()
-                    .map_err(|e| FfiError::internal(format!("Failed to create runtime: {}", e)))?
-                    .block_on(async move {
-                        CdkMultiMintWallet::new(localstore, seed, unit.into()).await
-                    })
-            }
-        }?;
-
-        Ok(Self {
-            inner: Arc::new(wallet),
-        })
-    }
-
-    /// Create a new MultiMintWallet with proxy configuration
-    #[uniffi::constructor]
-    pub fn new_with_proxy(
-        unit: CurrencyUnit,
-        mnemonic: String,
-        db: Arc<dyn crate::database::WalletDatabase>,
-        proxy_url: String,
-    ) -> Result<Self, FfiError> {
-        // Parse mnemonic and generate seed without passphrase
-        let m = Mnemonic::parse(&mnemonic)
-            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
-        let seed = m.to_seed_normalized("");
-
-        // Convert the FFI database trait to a CDK database implementation
-        let localstore = crate::database::create_cdk_database_from_ffi(db);
-
-        // Parse proxy URL
-        let proxy_url = url::Url::parse(&proxy_url)
-            .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;
-
-        let wallet = match tokio::runtime::Handle::try_current() {
-            Ok(handle) => tokio::task::block_in_place(|| {
-                handle.block_on(async move {
-                    CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
-                        .await
-                })
-            }),
-            Err(_) => {
-                // No current runtime, create a new one
-                tokio::runtime::Runtime::new()
-                    .map_err(|e| FfiError::internal(format!("Failed to create runtime: {}", e)))?
-                    .block_on(async move {
-                        CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
-                            .await
-                    })
-            }
-        }?;
-
-        Ok(Self {
-            inner: Arc::new(wallet),
-        })
-    }
-
-    /// Get the currency unit for this wallet
-    pub fn unit(&self) -> CurrencyUnit {
-        self.inner.unit().clone().into()
-    }
-
-    /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
-    ///
-    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
-    /// before requiring a refresh from the mint server for a specific mint.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint URL to set the TTL for
-    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
-    pub async fn set_metadata_cache_ttl_for_mint(
-        &self,
-        mint_url: MintUrl,
-        ttl_secs: Option<u64>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let wallets = self.inner.get_wallets().await;
-
-        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
-            let ttl = ttl_secs.map(std::time::Duration::from_secs);
-            wallet.set_metadata_cache_ttl(ttl);
-            Ok(())
-        } else {
-            Err(FfiError::internal(format!(
-                "Mint not found: {}",
-                cdk_mint_url
-            )))
-        }
-    }
-
-    /// Set metadata cache TTL (time-to-live) in seconds for all mints
-    ///
-    /// Controls how long cached mint metadata is considered fresh for all mints
-    /// in this MultiMintWallet.
-    ///
-    /// # Arguments
-    ///
-    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
-    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
-        let wallets = self.inner.get_wallets().await;
-        let ttl = ttl_secs.map(std::time::Duration::from_secs);
-
-        for wallet in wallets.iter() {
-            wallet.set_metadata_cache_ttl(ttl);
-        }
-    }
-
-    /// Add a mint to this MultiMintWallet
-    pub async fn add_mint(
-        &self,
-        mint_url: MintUrl,
-        target_proof_count: Option<u32>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-
-        if let Some(count) = target_proof_count {
-            let config = cdk::wallet::multi_mint_wallet::WalletConfig::new()
-                .with_target_proof_count(count as usize);
-            self.inner
-                .add_mint_with_config(cdk_mint_url, config)
-                .await?;
-        } else {
-            self.inner.add_mint(cdk_mint_url).await?;
-        }
-        Ok(())
-    }
-
-    /// Remove mint from MultiMintWallet
-    ///
-    /// # Panics
-    ///
-    /// Panics if the hardcoded fallback URL is invalid (should never happen).
-    pub async fn remove_mint(&self, mint_url: MintUrl) {
-        let url_str = mint_url.url.clone();
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().unwrap_or_else(|_| {
-            // If conversion fails, we can't remove the mint, but we shouldn't panic
-            // This is a best-effort operation
-            cdk::mint_url::MintUrl::from_str(&url_str).unwrap_or_else(|_| {
-                // Last resort: create a dummy URL that won't match anything
-                cdk::mint_url::MintUrl::from_str("https://invalid.mint")
-                    .expect("Valid hardcoded URL")
-            })
-        });
-        self.inner.remove_mint(&cdk_mint_url).await;
-    }
-
-    /// Check if mint is in wallet
-    pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
-        if let Ok(cdk_mint_url) = mint_url.try_into() {
-            self.inner.has_mint(&cdk_mint_url).await
-        } else {
-            false
-        }
-    }
-
-    pub async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Vec<KeySetInfo>, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let keysets = self.inner.get_mint_keysets(&cdk_mint_url).await?;
-
-        let keysets = keysets.into_iter().map(|k| k.into()).collect();
-
-        Ok(keysets)
-    }
-
-    /// Get token data (mint URL and proofs) from a token
-    ///
-    /// This method extracts the mint URL and proofs from a token. It will automatically
-    /// fetch the keysets from the mint if needed to properly decode the proofs.
-    ///
-    /// The mint must already be added to the wallet. If the mint is not in the wallet,
-    /// use `add_mint` first.
-    pub async fn get_token_data(&self, token: Arc<Token>) -> Result<TokenData, FfiError> {
-        let token_data = self.inner.get_token_data(&token.inner).await?;
-        Ok(token_data.into())
-    }
-
-    /// Get wallet balances for all mints
-    pub async fn get_balances(&self) -> Result<BalanceMap, FfiError> {
-        let balances = self.inner.get_balances().await?;
-        let mut balance_map = HashMap::new();
-        for (mint_url, amount) in balances {
-            balance_map.insert(mint_url.to_string(), amount.into());
-        }
-        Ok(balance_map)
-    }
-
-    /// Get total balance across all mints
-    pub async fn total_balance(&self) -> Result<Amount, FfiError> {
-        let total = self.inner.total_balance().await?;
-        Ok(total.into())
-    }
-
-    /// List proofs for all mints
-    pub async fn list_proofs(&self) -> Result<ProofsByMint, FfiError> {
-        let proofs = self.inner.list_proofs().await?;
-        let mut proofs_by_mint = HashMap::new();
-        for (mint_url, mint_proofs) in proofs {
-            let ffi_proofs: Vec<Proof> = mint_proofs.into_iter().map(|p| p.into()).collect();
-            proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
-        }
-        Ok(proofs_by_mint)
-    }
-
-    /// Check the state of proofs at a specific mint
-    pub async fn check_proofs_state(
-        &self,
-        mint_url: MintUrl,
-        proofs: Proofs,
-    ) -> Result<Vec<ProofState>, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
-            proofs.into_iter().map(|p| p.try_into()).collect();
-        let cdk_proofs = cdk_proofs?;
-
-        let states = self
-            .inner
-            .check_proofs_state(&cdk_mint_url, cdk_proofs)
-            .await?;
-
-        Ok(states.into_iter().map(|s| s.into()).collect())
-    }
-
-    /// Receive token
-    pub async fn receive(
-        &self,
-        token: Arc<Token>,
-        options: MultiMintReceiveOptions,
-    ) -> Result<Amount, FfiError> {
-        let amount = self
-            .inner
-            .receive(&token.to_string(), options.into())
-            .await?;
-        Ok(amount.into())
-    }
-
-    /// Restore wallets for a specific mint
-    pub async fn restore(&self, mint_url: MintUrl) -> Result<Restored, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let restored = self.inner.restore(&cdk_mint_url).await?;
-        Ok(restored.into())
-    }
-
-    /// Get all pending send operations across all mints
-    pub async fn get_pending_sends(&self) -> Result<Vec<PendingSend>, FfiError> {
-        let sends = self.inner.get_pending_sends().await?;
-        Ok(sends
-            .into_iter()
-            .map(|(mint_url, id)| PendingSend {
-                mint_url: mint_url.into(),
-                operation_id: id.to_string(),
-            })
-            .collect())
-    }
-
-    /// Revoke a pending send operation for a specific mint
-    pub async fn revoke_send(
-        &self,
-        mint_url: MintUrl,
-        operation_id: String,
-    ) -> Result<Amount, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let uuid = uuid::Uuid::parse_str(&operation_id)
-            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
-        let amount = self.inner.revoke_send(cdk_mint_url, uuid).await?;
-        Ok(amount.into())
-    }
-
-    /// Check status of a pending send operation for a specific mint
-    pub async fn check_send_status(
-        &self,
-        mint_url: MintUrl,
-        operation_id: String,
-    ) -> Result<bool, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let uuid = uuid::Uuid::parse_str(&operation_id)
-            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
-        let claimed = self.inner.check_send_status(cdk_mint_url, uuid).await?;
-        Ok(claimed)
-    }
-
-    /// Send tokens from a specific mint
-    ///
-    /// This method prepares and confirms the send in one step.
-    /// For more control over the send process, use the single-mint Wallet.
-    pub async fn send(
-        &self,
-        mint_url: MintUrl,
-        amount: Amount,
-        options: MultiMintSendOptions,
-    ) -> Result<Token, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let token = self
-            .inner
-            .send(cdk_mint_url, amount.into(), options.into())
-            .await?;
-        Ok(token.into())
-    }
-
-    /// Get a mint quote from a specific mint
-    pub async fn mint_quote(
-        &self,
-        mint_url: MintUrl,
-        payment_method: PaymentMethod,
-        amount: Option<Amount>,
-        description: Option<String>,
-        extra: Option<String>,
-    ) -> Result<MintQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let quote = self
-            .inner
-            .mint_quote(
-                &cdk_mint_url,
-                payment_method,
-                amount.map(Into::into),
-                description,
-                extra,
-            )
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Refresh a specific mint quote status from the mint.
-    /// Updates local store with current state from mint.
-    /// Does NOT mint tokens - use mint() to mint a specific quote.
-    pub async fn refresh_mint_quote(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-    ) -> Result<MintQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let quote = self
-            .inner
-            .refresh_mint_quote(&cdk_mint_url, &quote_id)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Fetch a mint quote from the mint and store it locally
-    ///
-    /// This method contacts the mint to get the current state of a quote,
-    /// creates or updates the quote in local storage, and returns the stored quote.
-    ///
-    /// Works with all payment methods (Bolt11, Bolt12, and custom payment methods).
-    ///
-    /// # Arguments
-    /// * `mint_url` - The URL of the mint
-    /// * `quote_id` - The ID of the quote to fetch
-    /// * `payment_method` - The payment method for the quote. Required if the quote
-    ///   is not already stored locally. If the quote exists locally, the stored
-    ///   payment method will be used and this parameter is ignored.
-    pub async fn fetch_mint_quote(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-        payment_method: Option<PaymentMethod>,
-    ) -> Result<MintQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let method = payment_method.map(Into::into);
-        let quote = self
-            .inner
-            .fetch_mint_quote(&cdk_mint_url, &quote_id, method)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Mint tokens at a specific mint
-    pub async fn mint(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-        spending_conditions: Option<SpendingConditions>,
-    ) -> Result<Proofs, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
-
-        let proofs = self
-            .inner
-            .mint(
-                &cdk_mint_url,
-                &quote_id,
-                cdk::amount::SplitTarget::default(),
-                conditions,
-            )
-            .await?;
-        Ok(proofs.into_iter().map(|p| p.into()).collect())
-    }
-
-    /// Wait for a mint quote to be paid and automatically mint the proofs
-    #[cfg(not(target_arch = "wasm32"))]
-    pub async fn wait_for_mint_quote(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-        split_target: SplitTarget,
-        spending_conditions: Option<SpendingConditions>,
-        timeout_secs: u64,
-    ) -> Result<Proofs, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
-        let timeout = std::time::Duration::from_secs(timeout_secs);
-
-        let proofs = self
-            .inner
-            .wait_for_mint_quote(
-                &cdk_mint_url,
-                &quote_id,
-                split_target.into(),
-                conditions,
-                timeout,
-            )
-            .await?;
-        Ok(proofs.into_iter().map(|p| p.into()).collect())
-    }
-
-    /// Get a melt quote from a specific mint
-    pub async fn melt_quote(
-        &self,
-        mint_url: MintUrl,
-        payment_method: PaymentMethod,
-        request: String,
-        options: Option<MeltOptions>,
-        extra: Option<String>,
-    ) -> Result<MeltQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_options = options.map(Into::into);
-        let quote = self
-            .inner
-            .melt_quote(&cdk_mint_url, payment_method, request, cdk_options, extra)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Get a melt quote for a BIP353 human-readable address
-    ///
-    /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
-    /// and then creates a melt quote for that offer at the specified mint.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `bip353_address` - Human-readable address in the format "user@domain.com"
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    #[cfg(not(target_arch = "wasm32"))]
-    pub async fn melt_bip353_quote(
-        &self,
-        mint_url: MintUrl,
-        bip353_address: String,
-        amount_msat: u64,
-    ) -> Result<MeltQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_amount = cdk::Amount::from(amount_msat);
-        let quote = self
-            .inner
-            .melt_bip353_quote(&cdk_mint_url, &bip353_address, cdk_amount)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Get a melt quote for a Lightning address
-    ///
-    /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
-    /// and then creates a melt quote for that invoice at the specified mint.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `lightning_address` - Lightning address in the format "user@domain.com"
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    pub async fn melt_lightning_address_quote(
-        &self,
-        mint_url: MintUrl,
-        lightning_address: String,
-        amount_msat: u64,
-    ) -> Result<MeltQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_amount = cdk::Amount::from(amount_msat);
-        let quote = self
-            .inner
-            .melt_lightning_address_quote(&cdk_mint_url, &lightning_address, cdk_amount)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Get a melt quote for a human-readable address
-    ///
-    /// This method accepts a human-readable address that could be either a BIP353 address
-    /// or a Lightning address. It intelligently determines which to try based on mint support:
-    ///
-    /// 1. If the mint supports Bolt12, it tries BIP353 first
-    /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
-    /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
-    /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `address` - Human-readable address (BIP353 or Lightning address)
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    #[cfg(not(target_arch = "wasm32"))]
-    pub async fn melt_human_readable_quote(
-        &self,
-        mint_url: MintUrl,
-        address: String,
-        amount_msat: u64,
-    ) -> Result<MeltQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_amount = cdk::Amount::from(amount_msat);
-        let quote = self
-            .inner
-            .melt_human_readable_quote(&cdk_mint_url, &address, cdk_amount)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Melt tokens
-    pub async fn melt_with_mint(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-    ) -> Result<FinalizedMelt, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let finalized = self.inner.melt_with_mint(&cdk_mint_url, &quote_id).await?;
-        Ok(finalized.into())
-    }
-
-    /// Melt specific proofs from a specific mint
-    ///
-    /// This method allows melting proofs that may not be in the wallet's database,
-    /// similar to how `receive_proofs` handles external proofs. The proofs will be
-    /// added to the database and used for the melt operation.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for the melt operation
-    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
-    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
-    ///
-    /// # Returns
-    ///
-    /// A `FinalizedMelt` result containing the payment details and any change proofs
-    pub async fn melt_proofs(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-        proofs: Proofs,
-    ) -> Result<FinalizedMelt, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
-            proofs.into_iter().map(|p| p.try_into()).collect();
-        let cdk_proofs = cdk_proofs?;
-
-        let finalized = self
-            .inner
-            .melt_proofs(&cdk_mint_url, &quote_id, cdk_proofs)
-            .await?;
-        Ok(finalized.into())
-    }
-
-    /// Check melt quote status
-    pub async fn check_melt_quote(
-        &self,
-        mint_url: MintUrl,
-        quote_id: String,
-    ) -> Result<MeltQuote, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let quote = self
-            .inner
-            .check_melt_quote(&cdk_mint_url, &quote_id)
-            .await?;
-        Ok(quote.into())
-    }
-
-    /// Melt tokens (pay a bolt11 invoice)
-    pub async fn melt(
-        &self,
-        bolt11: String,
-        options: Option<MeltOptions>,
-        max_fee: Option<Amount>,
-    ) -> Result<FinalizedMelt, FfiError> {
-        let cdk_options = options.map(Into::into);
-        let cdk_max_fee = max_fee.map(Into::into);
-        let finalized = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
-        Ok(finalized.into())
-    }
-
-    /// Transfer funds between mints
-    pub async fn transfer(
-        &self,
-        source_mint: MintUrl,
-        target_mint: MintUrl,
-        transfer_mode: TransferMode,
-    ) -> Result<TransferResult, FfiError> {
-        let source_cdk: cdk::mint_url::MintUrl = source_mint.try_into()?;
-        let target_cdk: cdk::mint_url::MintUrl = target_mint.try_into()?;
-        let result = self
-            .inner
-            .transfer(&source_cdk, &target_cdk, transfer_mode.into())
-            .await?;
-        Ok(result.into())
-    }
-
-    /// Swap proofs with automatic wallet selection
-    pub async fn swap(
-        &self,
-        amount: Option<Amount>,
-        spending_conditions: Option<SpendingConditions>,
-    ) -> Result<Option<Proofs>, FfiError> {
-        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
-
-        let result = self.inner.swap(amount.map(Into::into), conditions).await?;
-
-        Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
-    }
-
-    /// List transactions from all mints
-    pub async fn list_transactions(
-        &self,
-        direction: Option<TransactionDirection>,
-    ) -> Result<Vec<Transaction>, FfiError> {
-        let cdk_direction = direction.map(Into::into);
-        let transactions = self.inner.list_transactions(cdk_direction).await?;
-        Ok(transactions.into_iter().map(Into::into).collect())
-    }
-
-    /// Get proofs for a transaction by transaction ID
-    ///
-    /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
-    /// it will only check that specific mint's wallet. Otherwise, it searches across all
-    /// wallets to find which mint the transaction belongs to.
-    ///
-    /// # Arguments
-    ///
-    /// * `id` - The transaction ID
-    /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
-    pub async fn get_proofs_for_transaction(
-        &self,
-        id: TransactionId,
-        mint_url: Option<MintUrl>,
-    ) -> Result<Vec<Proof>, FfiError> {
-        let cdk_id = id.try_into()?;
-        let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
-        let proofs = self
-            .inner
-            .get_proofs_for_transaction(cdk_id, cdk_mint_url)
-            .await?;
-        Ok(proofs.into_iter().map(Into::into).collect())
-    }
-
-    /// Refresh all unissued mint quote states
-    /// Does NOT mint - use mint_unissued_quotes() for that
-    pub async fn refresh_all_mint_quotes(
-        &self,
-        mint_url: Option<MintUrl>,
-    ) -> Result<Vec<MintQuote>, FfiError> {
-        let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
-        let quotes = self.inner.refresh_all_mint_quotes(cdk_mint_url).await?;
-        Ok(quotes.into_iter().map(Into::into).collect())
-    }
-
-    /// Refresh states and mint all unissued quotes
-    /// Returns total amount minted across all wallets
-    pub async fn mint_unissued_quotes(
-        &self,
-        mint_url: Option<MintUrl>,
-    ) -> Result<Amount, FfiError> {
-        let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
-        let amount = self.inner.mint_unissued_quotes(cdk_mint_url).await?;
-        Ok(amount.into())
-    }
-
-    /// Consolidate proofs across all mints
-    pub async fn consolidate(&self) -> Result<Amount, FfiError> {
-        let amount = self.inner.consolidate().await?;
-        Ok(amount.into())
-    }
-
-    /// Get list of mint URLs
-    pub async fn get_mint_urls(&self) -> Vec<String> {
-        let wallets = self.inner.get_wallets().await;
-        wallets.iter().map(|w| w.mint_url.to_string()).collect()
-    }
-
-    /// Get all wallets from MultiMintWallet
-    pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
-        let wallets = self.inner.get_wallets().await;
-        wallets
-            .into_iter()
-            .map(|w| Arc::new(crate::wallet::Wallet::from_inner(w)))
-            .collect()
-    }
-
-    /// Get a specific wallet from MultiMintWallet by mint URL
-    pub async fn get_wallet(&self, mint_url: MintUrl) -> Option<Arc<crate::wallet::Wallet>> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into().ok()?;
-        let wallet = self.inner.get_wallet(&cdk_mint_url).await?;
-        Some(Arc::new(crate::wallet::Wallet::from_inner(wallet)))
-    }
-
-    /// Verify token DLEQ proofs
-    pub async fn verify_token_dleq(&self, token: Arc<Token>) -> Result<(), FfiError> {
-        let cdk_token = token.inner.clone();
-        self.inner.verify_token_dleq(&cdk_token).await?;
-        Ok(())
-    }
-
-    /// Query mint for current mint information
-    pub async fn fetch_mint_info(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let mint_info = self.inner.fetch_mint_info(&cdk_mint_url).await?;
-        Ok(mint_info.map(Into::into))
-    }
-
-    /// Get mint info for all wallets
-    ///
-    /// This method loads the mint info for each wallet in the MultiMintWallet
-    /// and returns a map of mint URLs to their corresponding mint info.
-    ///
-    /// Uses cached mint info when available, only fetching from the mint if the cache
-    /// has expired.
-    pub async fn get_all_mint_info(&self) -> Result<MintInfoMap, FfiError> {
-        let mint_infos = self.inner.get_all_mint_info().await?;
-        let mut result = HashMap::new();
-        for (mint_url, mint_info) in mint_infos {
-            result.insert(mint_url.to_string(), mint_info.into());
-        }
-        Ok(result)
-    }
-}
-
-/// Payment request methods for MultiMintWallet
-#[uniffi::export(async_runtime = "tokio")]
-impl MultiMintWallet {
-    /// Pay a NUT-18 PaymentRequest
-    ///
-    /// This method handles paying a payment request by selecting an appropriate mint:
-    /// - If `mint_url` is provided, it verifies the payment request accepts that mint
-    ///   and uses it to pay.
-    /// - If `mint_url` is None, it automatically selects the mint that:
-    ///   1. Is accepted by the payment request (matches one of the request's mints, or request accepts any mint)
-    ///   2. Has the highest balance among matching mints
-    ///
-    /// # Arguments
-    ///
-    /// * `payment_request` - The NUT-18 payment request to pay
-    /// * `mint_url` - Optional specific mint to use. If None, automatically selects the best matching mint.
-    /// * `custom_amount` - Custom amount to pay (required if payment request has no amount)
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if:
-    /// - The payment request has no amount and no custom amount is provided
-    /// - The specified mint is not accepted by the payment request
-    /// - No matching mint has sufficient balance
-    /// - No transport is available in the payment request
-    pub async fn pay_request(
-        &self,
-        payment_request: Arc<PaymentRequest>,
-        mint_url: Option<MintUrl>,
-        custom_amount: Option<Amount>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
-        let cdk_amount = custom_amount.map(Into::into);
-
-        self.inner
-            .pay_request(payment_request.inner().clone(), cdk_mint_url, cdk_amount)
-            .await?;
-
-        Ok(())
-    }
-
-    /// Create a NUT-18 payment request
-    ///
-    /// Creates a payment request that can be shared to receive Cashu tokens.
-    /// The request can include optional amount, description, and spending conditions.
-    ///
-    /// # Arguments
-    ///
-    /// * `params` - Parameters for creating the payment request
-    ///
-    /// # Transport Options
-    ///
-    /// - `"nostr"` - Uses Nostr relays for privacy-preserving delivery (requires nostr_relays)
-    /// - `"http"` - Uses HTTP POST for delivery (requires http_url)
-    /// - `"none"` - No transport; token must be delivered out-of-band
-    ///
-    /// # Example
-    ///
-    /// ```ignore
-    /// let params = CreateRequestParams {
-    ///     amount: Some(100),
-    ///     unit: "sat".to_string(),
-    ///     description: Some("Coffee payment".to_string()),
-    ///     transport: "http".to_string(),
-    ///     http_url: Some("https://example.com/callback".to_string()),
-    ///     ..Default::default()
-    /// };
-    /// let result = wallet.create_request(params).await?;
-    /// println!("Share this request: {}", result.payment_request.to_string_encoded());
-    ///
-    /// // If using Nostr transport, wait for payment:
-    /// if let Some(nostr_info) = result.nostr_wait_info {
-    ///     let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
-    ///     println!("Received {} sats", amount);
-    /// }
-    /// ```
-    pub async fn create_request(
-        &self,
-        params: CreateRequestParams,
-    ) -> Result<CreateRequestResult, FfiError> {
-        let (payment_request, nostr_wait_info) = self.inner.create_request(params.into()).await?;
-        Ok(CreateRequestResult {
-            payment_request: Arc::new(PaymentRequest::from_inner(payment_request)),
-            nostr_wait_info: nostr_wait_info.map(|info| Arc::new(NostrWaitInfo::from_inner(info))),
-        })
-    }
-
-    /// Wait for a Nostr payment and receive it into the wallet
-    ///
-    /// This method connects to the Nostr relays specified in the `NostrWaitInfo`,
-    /// subscribes for incoming payment events, and receives the first valid
-    /// payment into the wallet.
-    ///
-    /// # Arguments
-    ///
-    /// * `info` - The Nostr wait info returned from `create_request` when using Nostr transport
-    ///
-    /// # Returns
-    ///
-    /// The amount received from the payment.
-    ///
-    /// # Example
-    ///
-    /// ```ignore
-    /// let result = wallet.create_request(params).await?;
-    /// if let Some(nostr_info) = result.nostr_wait_info {
-    ///     let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
-    ///     println!("Received {} sats", amount);
-    /// }
-    /// ```
-    pub async fn wait_for_nostr_payment(
-        &self,
-        info: Arc<NostrWaitInfo>,
-    ) -> Result<Amount, FfiError> {
-        // We need to clone the inner NostrWaitInfo since we can't consume the Arc
-        let info_inner = cdk::wallet::payment_request::NostrWaitInfo {
-            keys: info.inner().keys.clone(),
-            relays: info.inner().relays.clone(),
-            pubkey: info.inner().pubkey,
-        };
-        let amount = self
-            .inner
-            .wait_for_nostr_payment(info_inner)
-            .await
-            .map_err(FfiError::internal)?;
-        Ok(amount.into())
-    }
-}
-
-/// Auth methods for MultiMintWallet
-#[uniffi::export(async_runtime = "tokio")]
-impl MultiMintWallet {
-    /// Set Clear Auth Token (CAT) for a specific mint
-    pub async fn set_cat(&self, mint_url: MintUrl, cat: String) -> Result<(), FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        self.inner.set_cat(&cdk_mint_url, cat).await?;
-        Ok(())
-    }
-
-    /// Set refresh token for a specific mint
-    pub async fn set_refresh_token(
-        &self,
-        mint_url: MintUrl,
-        refresh_token: String,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        self.inner
-            .set_refresh_token(&cdk_mint_url, refresh_token)
-            .await?;
-        Ok(())
-    }
-
-    /// Refresh access token for a specific mint using the stored refresh token
-    pub async fn refresh_access_token(&self, mint_url: MintUrl) -> Result<(), FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        self.inner.refresh_access_token(&cdk_mint_url).await?;
-        Ok(())
-    }
-
-    /// Mint blind auth tokens at a specific mint
-    pub async fn mint_blind_auth(
-        &self,
-        mint_url: MintUrl,
-        amount: Amount,
-    ) -> Result<Proofs, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let proofs = self
-            .inner
-            .mint_blind_auth(&cdk_mint_url, amount.into())
-            .await?;
-        Ok(proofs.into_iter().map(|p| p.into()).collect())
-    }
-
-    /// Get unspent auth proofs for a specific mint
-    pub async fn get_unspent_auth_proofs(
-        &self,
-        mint_url: MintUrl,
-    ) -> Result<Vec<AuthProof>, FfiError> {
-        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let auth_proofs = self.inner.get_unspent_auth_proofs(&cdk_mint_url).await?;
-        Ok(auth_proofs.into_iter().map(Into::into).collect())
-    }
-}
-
-/// Transfer mode for mint-to-mint transfers
-#[derive(Debug, Clone, uniffi::Enum)]
-pub enum TransferMode {
-    /// Transfer exact amount to target (target receives specified amount)
-    ExactReceive { amount: Amount },
-    /// Transfer all available balance (source will be emptied)
-    FullBalance,
-}
-
-impl From<TransferMode> for CdkTransferMode {
-    fn from(mode: TransferMode) -> Self {
-        match mode {
-            TransferMode::ExactReceive { amount } => CdkTransferMode::ExactReceive(amount.into()),
-            TransferMode::FullBalance => CdkTransferMode::FullBalance,
-        }
-    }
-}
-
-/// Result of a transfer operation with detailed breakdown
-#[derive(Debug, Clone, uniffi::Record)]
-pub struct TransferResult {
-    /// Amount deducted from source mint
-    pub amount_sent: Amount,
-    /// Amount received at target mint
-    pub amount_received: Amount,
-    /// Total fees paid for the transfer
-    pub fees_paid: Amount,
-    /// Remaining balance in source mint after transfer
-    pub source_balance_after: Amount,
-    /// New balance in target mint after transfer
-    pub target_balance_after: Amount,
-}
-
-impl From<CdkTransferResult> for TransferResult {
-    fn from(result: CdkTransferResult) -> Self {
-        Self {
-            amount_sent: result.amount_sent.into(),
-            amount_received: result.amount_received.into(),
-            fees_paid: result.fees_paid.into(),
-            source_balance_after: result.source_balance_after.into(),
-            target_balance_after: result.target_balance_after.into(),
-        }
-    }
-}
-
-/// Represents a pending send operation
-#[derive(Debug, Clone, uniffi::Record)]
-pub struct PendingSend {
-    /// The mint URL where the send is pending
-    pub mint_url: MintUrl,
-    /// The operation ID of the pending send
-    pub operation_id: String,
-}
-
-/// Data extracted from a token including mint URL, proofs, and memo
-#[derive(Debug, Clone, uniffi::Record)]
-pub struct TokenData {
-    /// The mint URL from the token
-    pub mint_url: MintUrl,
-    /// The proofs contained in the token
-    pub proofs: Proofs,
-    /// The memo from the token, if present
-    pub memo: Option<String>,
-    /// Value of token
-    pub value: Amount,
-    /// Unit of token
-    pub unit: CurrencyUnit,
-    /// Fee to redeem
-    ///
-    /// If the token is for a proof that we do not know, we cannot get the fee.
-    /// To avoid just erroring and still allow decoding, this is an option.
-    /// None does not mean there is no fee, it means we do not know the fee.
-    pub redeem_fee: Option<Amount>,
-}
-
-impl From<CdkTokenData> for TokenData {
-    fn from(data: CdkTokenData) -> Self {
-        Self {
-            mint_url: data.mint_url.into(),
-            proofs: data.proofs.into_iter().map(|p| p.into()).collect(),
-            memo: data.memo,
-            value: data.value.into(),
-            unit: data.unit.into(),
-            redeem_fee: data.redeem_fee.map(|a| a.into()),
-        }
-    }
-}
-
-/// Options for receiving tokens in multi-mint context
-#[derive(Debug, Clone, Default, uniffi::Record)]
-pub struct MultiMintReceiveOptions {
-    /// Whether to allow receiving from untrusted (not yet added) mints
-    pub allow_untrusted: bool,
-    /// Mint URL to transfer tokens to from untrusted mints (None means keep in original mint)
-    pub transfer_to_mint: Option<MintUrl>,
-    /// Base receive options to apply to the wallet receive
-    pub receive_options: ReceiveOptions,
-}
-
-impl From<MultiMintReceiveOptions> for CdkMultiMintReceiveOptions {
-    fn from(options: MultiMintReceiveOptions) -> Self {
-        let mut opts = CdkMultiMintReceiveOptions::new();
-        opts.allow_untrusted = options.allow_untrusted;
-        opts.transfer_to_mint = options.transfer_to_mint.and_then(|url| url.try_into().ok());
-        opts.receive_options = options.receive_options.into();
-        opts
-    }
-}
-
-/// Options for sending tokens in multi-mint context
-#[derive(Debug, Clone, Default, uniffi::Record)]
-pub struct MultiMintSendOptions {
-    /// Whether to allow transferring funds from other mints if needed
-    pub allow_transfer: bool,
-    /// Maximum amount to transfer from other mints (optional limit)
-    pub max_transfer_amount: Option<Amount>,
-    /// Specific mint URLs allowed for transfers (empty means all mints allowed)
-    pub allowed_mints: Vec<MintUrl>,
-    /// Specific mint URLs to exclude from transfers
-    pub excluded_mints: Vec<MintUrl>,
-    /// Base send options to apply to the wallet send
-    pub send_options: SendOptions,
-}
-
-impl From<MultiMintSendOptions> for CdkMultiMintSendOptions {
-    fn from(options: MultiMintSendOptions) -> Self {
-        let mut opts = CdkMultiMintSendOptions::new();
-        opts.allow_transfer = options.allow_transfer;
-        opts.max_transfer_amount = options.max_transfer_amount.map(Into::into);
-        opts.allowed_mints = options
-            .allowed_mints
-            .into_iter()
-            .filter_map(|url| url.try_into().ok())
-            .collect();
-        opts.excluded_mints = options
-            .excluded_mints
-            .into_iter()
-            .filter_map(|url| url.try_into().ok())
-            .collect();
-        opts.send_options = options.send_options.into();
-        opts
-    }
-}
-
-/// Nostr backup methods for MultiMintWallet (NUT-XX)
-#[uniffi::export(async_runtime = "tokio")]
-impl MultiMintWallet {
-    /// Get the hex-encoded public key used for Nostr mint backup
-    ///
-    /// This key is deterministically derived from the wallet seed and can be used
-    /// to identify and decrypt backup events on Nostr relays.
-    pub fn backup_public_key(&self) -> Result<String, FfiError> {
-        let keys = self.inner.backup_keys()?;
-        Ok(keys.public_key().to_hex())
-    }
-
-    /// Backup the current mint list to Nostr relays
-    ///
-    /// Creates an encrypted NIP-78 addressable event containing all mint URLs
-    /// and publishes it to the specified relays.
-    ///
-    /// # Arguments
-    ///
-    /// * `relays` - List of Nostr relay URLs (e.g., "wss://relay.damus.io")
-    /// * `options` - Backup options including optional client name
-    ///
-    /// # Example
-    ///
-    /// ```ignore
-    /// let relays = vec!["wss://relay.damus.io".to_string(), "wss://nos.lol".to_string()];
-    /// let options = BackupOptions { client: Some("my-wallet".to_string()) };
-    /// let result = wallet.backup_mints(relays, options).await?;
-    /// println!("Backup published with event ID: {}", result.event_id);
-    /// ```
-    pub async fn backup_mints(
-        &self,
-        relays: Vec<String>,
-        options: BackupOptions,
-    ) -> Result<BackupResult, FfiError> {
-        let result = self.inner.backup_mints(relays, options.into()).await?;
-        Ok(result.into())
-    }
-
-    /// Restore mint list from Nostr relays
-    ///
-    /// Fetches the most recent backup event from the specified relays,
-    /// decrypts it, and optionally adds the discovered mints to the wallet.
-    ///
-    /// # Arguments
-    ///
-    /// * `relays` - List of Nostr relay URLs to fetch from
-    /// * `add_mints` - If true, automatically add discovered mints to the wallet
-    /// * `options` - Restore options including timeout
-    ///
-    /// # Example
-    ///
-    /// ```ignore
-    /// let relays = vec!["wss://relay.damus.io".to_string()];
-    /// let result = wallet.restore_mints(relays, true, RestoreOptions::default()).await?;
-    /// println!("Restored {} mints, {} newly added", result.mint_count, result.mints_added);
-    /// ```
-    pub async fn restore_mints(
-        &self,
-        relays: Vec<String>,
-        add_mints: bool,
-        options: RestoreOptions,
-    ) -> Result<RestoreResult, FfiError> {
-        let result = self
-            .inner
-            .restore_mints(relays, add_mints, options.into())
-            .await?;
-        Ok(result.into())
-    }
-
-    /// Fetch the backup without adding mints to the wallet
-    ///
-    /// This is useful for previewing what mints are in the backup before
-    /// deciding to add them.
-    ///
-    /// # Arguments
-    ///
-    /// * `relays` - List of Nostr relay URLs to fetch from
-    /// * `options` - Restore options including timeout
-    pub async fn fetch_backup(
-        &self,
-        relays: Vec<String>,
-        options: RestoreOptions,
-    ) -> Result<MintBackup, FfiError> {
-        let backup = self.inner.fetch_backup(relays, options.into()).await?;
-        Ok(backup.into())
-    }
-}
-
-/// Type alias for balances by mint URL
-pub type BalanceMap = HashMap<String, Amount>;
-
-/// Type alias for proofs by mint URL
-pub type ProofsByMint = HashMap<String, Vec<Proof>>;
-
-/// Type alias for mint info by mint URL
-pub type MintInfoMap = HashMap<String, MintInfo>;

+ 1 - 10
crates/cdk-ffi/src/types/payment_request.rs

@@ -77,11 +77,6 @@ pub struct PaymentRequest {
 }
 
 impl PaymentRequest {
-    /// Create from inner CDK type
-    pub(crate) fn from_inner(inner: cdk::nuts::PaymentRequest) -> Self {
-        Self { inner }
-    }
-
     /// Get inner reference
     pub(crate) fn inner(&self) -> &cdk::nuts::PaymentRequest {
         &self.inner
@@ -252,12 +247,8 @@ pub struct NostrWaitInfo {
 }
 
 impl NostrWaitInfo {
-    /// Create from inner CDK type
-    pub(crate) fn from_inner(inner: cdk::wallet::payment_request::NostrWaitInfo) -> Self {
-        Self { inner }
-    }
-
     /// Get inner reference
+    #[allow(dead_code)]
     pub(crate) fn inner(&self) -> &cdk::wallet::payment_request::NostrWaitInfo {
         &self.inner
     }

+ 1 - 0
crates/cdk-ffi/src/wallet.rs

@@ -12,6 +12,7 @@ use crate::types::payment_request::PaymentRequest;
 use crate::types::*;
 
 /// FFI-compatible Wallet
+
 #[derive(uniffi::Object)]
 pub struct Wallet {
     inner: Arc<CdkWallet>,

+ 277 - 0
crates/cdk-ffi/src/wallet_repository.rs

@@ -0,0 +1,277 @@
+//! FFI WalletRepository bindings
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cdk::wallet::wallet_repository::{
+    WalletRepository as CdkWalletRepository, WalletRepositoryBuilder,
+};
+
+use crate::error::FfiError;
+use crate::types::*;
+
+/// FFI-compatible WalletRepository
+#[derive(uniffi::Object)]
+pub struct WalletRepository {
+    inner: Arc<CdkWalletRepository>,
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+impl WalletRepository {
+    /// Create a new WalletRepository from mnemonic using WalletDatabaseFfi trait
+    #[uniffi::constructor]
+    pub fn new(
+        mnemonic: String,
+        db: Arc<dyn crate::database::WalletDatabase>,
+    ) -> Result<Self, FfiError> {
+        // Parse mnemonic and generate seed without passphrase
+        let m = Mnemonic::parse(&mnemonic)
+            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
+        let seed = m.to_seed_normalized("");
+
+        // Convert the FFI database trait to a CDK database implementation
+        let localstore = crate::database::create_cdk_database_from_ffi(db);
+
+        let wallet = match tokio::runtime::Handle::try_current() {
+            Ok(handle) => tokio::task::block_in_place(|| {
+                handle.block_on(async move {
+                    WalletRepositoryBuilder::new()
+                        .localstore(localstore)
+                        .seed(seed)
+                        .build()
+                        .await
+                })
+            }),
+            Err(_) => {
+                // No current runtime, create a new one
+                tokio::runtime::Runtime::new()
+                    .map_err(|e| FfiError::internal(format!("Failed to create runtime: {}", e)))?
+                    .block_on(async move {
+                        WalletRepositoryBuilder::new()
+                            .localstore(localstore)
+                            .seed(seed)
+                            .build()
+                            .await
+                    })
+            }
+        }?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Create a new WalletRepository with proxy configuration
+    #[uniffi::constructor]
+    pub fn new_with_proxy(
+        mnemonic: String,
+        db: Arc<dyn crate::database::WalletDatabase>,
+        proxy_url: String,
+    ) -> Result<Self, FfiError> {
+        // Parse mnemonic and generate seed without passphrase
+        let m = Mnemonic::parse(&mnemonic)
+            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
+        let seed = m.to_seed_normalized("");
+
+        // Convert the FFI database trait to a CDK database implementation
+        let localstore = crate::database::create_cdk_database_from_ffi(db);
+
+        // Parse proxy URL
+        let proxy_url = url::Url::parse(&proxy_url)
+            .map_err(|e| FfiError::internal(format!("Invalid URL: {}", e)))?;
+
+        let wallet = match tokio::runtime::Handle::try_current() {
+            Ok(handle) => tokio::task::block_in_place(|| {
+                handle.block_on(async move {
+                    WalletRepositoryBuilder::new()
+                        .localstore(localstore)
+                        .seed(seed)
+                        .proxy_url(proxy_url)
+                        .build()
+                        .await
+                })
+            }),
+            Err(_) => {
+                // No current runtime, create a new one
+                tokio::runtime::Runtime::new()
+                    .map_err(|e| FfiError::internal(format!("Failed to create runtime: {}", e)))?
+                    .block_on(async move {
+                        WalletRepositoryBuilder::new()
+                            .localstore(localstore)
+                            .seed(seed)
+                            .proxy_url(proxy_url)
+                            .build()
+                            .await
+                    })
+            }
+        }?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
+    ///
+    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
+    /// before requiring a refresh from the mint server for a specific mint.
+    ///
+    /// # Arguments
+    ///
+    /// * `mint_url` - The mint URL to set the TTL for
+    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
+    pub async fn set_metadata_cache_ttl_for_mint(
+        &self,
+        mint_url: MintUrl,
+        ttl_secs: Option<u64>,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let wallets = self.inner.get_wallets().await;
+
+        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
+            let ttl = ttl_secs.map(std::time::Duration::from_secs);
+            wallet.set_metadata_cache_ttl(ttl);
+            Ok(())
+        } else {
+            Err(FfiError::internal(format!(
+                "Mint not found: {}",
+                cdk_mint_url
+            )))
+        }
+    }
+
+    /// Set metadata cache TTL (time-to-live) in seconds for all mints
+    ///
+    /// Controls how long cached mint metadata is considered fresh for all mints
+    /// in this WalletRepository.
+    ///
+    /// # Arguments
+    ///
+    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
+    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
+        let wallets = self.inner.get_wallets().await;
+        let ttl = ttl_secs.map(std::time::Duration::from_secs);
+
+        for wallet in wallets.iter() {
+            wallet.set_metadata_cache_ttl(ttl);
+        }
+    }
+
+    /// Add a mint to this WalletRepository
+    pub async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        unit: Option<CurrencyUnit>,
+        target_proof_count: Option<u32>,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+
+        let config = target_proof_count.map(|count| {
+            cdk::wallet::wallet_repository::WalletConfig::new()
+                .with_target_proof_count(count as usize)
+        });
+
+        let unit_enum = unit.unwrap_or(CurrencyUnit::Sat);
+
+        self.inner
+            .create_wallet(cdk_mint_url, unit_enum.into(), config)
+            .await?;
+
+        Ok(())
+    }
+
+    /// Remove mint from WalletRepository
+    pub async fn remove_mint(
+        &self,
+        mint_url: MintUrl,
+        currency_unit: CurrencyUnit,
+    ) -> Result<(), FfiError> {
+        // 1. Convert MintUrl safely without unwrap()
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
+            .try_into()
+            .map_err(|_| FfiError::internal("invalid mint url"))?; // Map the error to your FfiError type
+
+        // 2. Await the inner call and propagate its result with '?'
+        self.inner
+            .remove_wallet(cdk_mint_url, currency_unit.into())
+            .await
+            .map_err(|e| e.into()) // Ensure the inner error can convert to FfiError
+    }
+
+    /// Check if mint is in wallet
+    pub async fn has_mint(&self, mint_url: MintUrl) -> bool {
+        if let Ok(cdk_mint_url) = mint_url.try_into() {
+            self.inner.has_mint(&cdk_mint_url).await
+        } else {
+            false
+        }
+    }
+
+    /// Get wallet balances for all mints
+    pub async fn get_balances(&self) -> Result<HashMap<String, Amount>, FfiError> {
+        let balances = self.inner.get_balances().await?;
+        let mut balance_map = HashMap::new();
+        for (mint_url, amount) in balances {
+            balance_map.insert(mint_url.to_string(), amount.into());
+        }
+        Ok(balance_map)
+    }
+
+    /// Get all wallets from WalletRepository
+    pub async fn get_wallets(&self) -> Vec<Arc<crate::wallet::Wallet>> {
+        let wallets = self.inner.get_wallets().await;
+        wallets
+            .into_iter()
+            .map(|w| Arc::new(crate::wallet::Wallet::from_inner(Arc::new(w))))
+            .collect()
+    }
+
+    /// Get a specific wallet from WalletRepository by mint URL
+    ///
+    /// Returns an error if no wallet exists for the given mint URL.
+    pub async fn get_wallet(
+        &self,
+        mint_url: MintUrl,
+        unit: CurrencyUnit,
+    ) -> Result<Arc<crate::wallet::Wallet>, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let unit_cdk: cdk::nuts::CurrencyUnit = unit.into();
+        let wallet = self.inner.get_wallet(&cdk_mint_url, &unit_cdk).await?;
+        Ok(Arc::new(crate::wallet::Wallet::from_inner(Arc::new(
+            wallet,
+        ))))
+    }
+}
+
+/// Token data FFI type
+///
+/// Contains information extracted from a parsed token.
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct TokenData {
+    /// The mint URL from the token
+    pub mint_url: MintUrl,
+    /// The proofs contained in the token
+    pub proofs: Vec<crate::types::Proof>,
+    /// The memo from the token, if present
+    pub memo: Option<String>,
+    /// Value of token in smallest unit
+    pub value: Amount,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Fee to redeem (None if unknown)
+    pub redeem_fee: Option<Amount>,
+}
+
+impl From<cdk::wallet::TokenData> for TokenData {
+    fn from(data: cdk::wallet::TokenData) -> Self {
+        Self {
+            mint_url: data.mint_url.into(),
+            proofs: data.proofs.into_iter().map(Into::into).collect(),
+            memo: data.memo,
+            value: data.value.into(),
+            unit: data.unit.into(),
+            redeem_fee: data.redeem_fee.map(Into::into),
+        }
+    }
+}

+ 66 - 22
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -23,7 +23,7 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::{KnownMethod, ProofsMethods};
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, PaymentMethod, State};
-use cdk::wallet::{HttpClient, MintConnector, MultiMintWallet, Wallet};
+use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletRepositoryBuilder};
 use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;
 use futures::{SinkExt, StreamExt};
@@ -136,7 +136,12 @@ async fn test_happy_mint_melt_round_trip() {
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
 
     let melt = wallet
-        .melt_quote(PaymentMethod::BOLT11, invoice, None, None)
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
@@ -670,7 +675,12 @@ async fn test_melt_quote_status_after_melt() {
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
 
     let melt_quote = wallet
-        .melt_quote(PaymentMethod::BOLT11, invoice, None, None)
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
@@ -719,20 +729,28 @@ async fn test_melt_quote_status_after_melt_multi_mint_wallet() {
     let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
     let localstore = Arc::new(memory::empty().await.unwrap());
 
-    let multi_mint_wallet = MultiMintWallet::new(localstore.clone(), seed, CurrencyUnit::Sat)
+    let multi_mint_wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore.clone())
+        .seed(seed)
+        .build()
         .await
         .expect("failed to create multi mint wallet");
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
     multi_mint_wallet
-        .add_mint(mint_url.clone())
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
-    let mint_quote = multi_mint_wallet
+    // Get the wallet from the repository to call methods directly
+    let wallet = multi_mint_wallet
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .expect("failed to get wallet");
+
+    let mint_quote = wallet
         .mint_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(100.into()),
             None,
             None,
@@ -745,10 +763,9 @@ async fn test_melt_quote_status_after_melt_multi_mint_wallet() {
         .await
         .unwrap();
 
-    let _proofs = multi_mint_wallet
-        .wait_for_mint_quote(
-            &mint_url,
-            &mint_quote.id,
+    let _proofs = wallet
+        .wait_and_mint_quote(
+            mint_quote.clone(),
             SplitTarget::default(),
             None,
             Duration::from_secs(60),
@@ -756,24 +773,36 @@ async fn test_melt_quote_status_after_melt_multi_mint_wallet() {
         .await
         .expect("mint failed");
 
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
+    let balances = multi_mint_wallet.total_balance().await.unwrap();
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance, 100.into());
 
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
 
-    let melt_quote = multi_mint_wallet
-        .melt_quote(&mint_url, PaymentMethod::BOLT11, invoice, None, None)
+    let melt_quote = wallet
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
-    let melt_response = multi_mint_wallet
-        .melt_with_mint(&mint_url, &melt_quote.id)
+    let melt_response: cdk::types::FinalizedMelt = wallet
+        .prepare_melt(&melt_quote.id, HashMap::new())
+        .await
+        .unwrap()
+        .confirm()
         .await
         .unwrap();
     assert_eq!(melt_response.state(), MeltQuoteState::Paid);
 
-    let quote_status = multi_mint_wallet
-        .check_melt_quote(&mint_url, &melt_quote.id)
+    let quote_status = wallet
+        .check_melt_quote_status(&melt_quote.id)
         .await
         .unwrap();
     assert_eq!(
@@ -842,7 +871,12 @@ async fn test_fake_melt_change_in_quote() {
     let proofs = wallet.get_unspent_proofs().await.unwrap();
 
     let melt_quote = wallet
-        .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
@@ -929,7 +963,12 @@ async fn test_pay_invoice_twice() {
     let invoice = create_invoice_for_env(Some(25)).await.unwrap();
 
     let melt_quote = wallet
-        .melt_quote(PaymentMethod::BOLT11, invoice.clone(), None, None)
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
@@ -941,7 +980,12 @@ async fn test_pay_invoice_twice() {
 
     // Creating a second quote for the same invoice is allowed
     let melt_quote_two = wallet
-        .melt_quote(PaymentMethod::BOLT11, invoice, None, None)
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
 

+ 323 - 241
crates/cdk-integration-tests/tests/multi_mint_wallet.rs → crates/cdk-integration-tests/tests/wallet_repository.rs

@@ -1,6 +1,6 @@
-//! Integration tests for MultiMintWallet
+//! Integration tests for WalletRepository
 //!
-//! These tests verify the multi-mint wallet functionality including:
+//! These tests verify the WalletRepository functionality including:
 //! - Basic mint/melt operations across multiple mints
 //! - Token receive and send operations
 //! - Automatic mint selection for melts
@@ -16,9 +16,10 @@ use std::sync::Arc;
 use bip39::Mnemonic;
 use cdk::amount::{Amount, SplitTarget};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::nut00::{KnownMethod, ProofsMethods};
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PaymentMethod, Token};
-use cdk::wallet::{MultiMintReceiveOptions, MultiMintWallet, SendOptions};
+use cdk::wallet::{ReceiveOptions, SendOptions, WalletRepository, WalletRepositoryBuilder};
+use cdk_common::wallet::WalletKey;
 use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
 use cdk_sqlite::wallet::memory;
 use lightning_invoice::Bolt11Invoice;
@@ -31,24 +32,36 @@ fn get_test_temp_dir() -> PathBuf {
     }
 }
 
-/// Helper to create a MultiMintWallet with a fresh seed and in-memory database
-async fn create_test_multi_mint_wallet() -> MultiMintWallet {
+// Helper to create a WalletRepository with a fresh seed and in-memory database
+async fn create_test_wallet_repository() -> cdk::wallet::WalletRepository {
     let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
     let localstore = Arc::new(memory::empty().await.unwrap());
 
-    MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
+    WalletRepositoryBuilder::new()
+        .localstore(localstore)
+        .seed(seed)
+        .build()
         .await
-        .expect("failed to create multi mint wallet")
+        .expect("failed to create wallet repository")
 }
 
-/// Helper to fund a MultiMintWallet at a specific mint
-async fn fund_multi_mint_wallet(
-    wallet: &MultiMintWallet,
+/// Helper to fund a WalletRepository at a specific mint
+async fn fund_wallet_repository(
+    repo: &WalletRepository,
     mint_url: &MintUrl,
     amount: Amount,
 ) -> Amount {
+    let wallet = repo
+        .get_wallet(mint_url, &CurrencyUnit::Sat)
+        .await
+        .expect("wallet not found");
     let mint_quote = wallet
-        .mint_quote(mint_url, PaymentMethod::BOLT11, Some(amount), None, None)
+        .mint_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            Some(amount),
+            None,
+            None,
+        )
         .await
         .unwrap();
 
@@ -58,9 +71,8 @@ async fn fund_multi_mint_wallet(
         .unwrap();
 
     let proofs = wallet
-        .wait_for_mint_quote(
-            mint_url,
-            &mint_quote.id,
+        .wait_and_mint_quote(
+            mint_quote,
             SplitTarget::default(),
             None,
             std::time::Duration::from_secs(60),
@@ -71,7 +83,7 @@ async fn fund_multi_mint_wallet(
     proofs.total_amount().unwrap()
 }
 
-/// Test the direct mint() function on MultiMintWallet
+/// Test the direct mint() function on WalletRepository
 ///
 /// This test verifies:
 /// 1. Create a mint quote
@@ -80,20 +92,24 @@ async fn fund_multi_mint_wallet(
 /// 4. Call mint() directly (not wait_for_mint_quote)
 /// 5. Verify tokens are received
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_mint() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_mint() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .expect("failed to get wallet");
+
     // Create mint quote
-    let mint_quote = multi_mint_wallet
+    let mint_quote = wallet
         .mint_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(100.into()),
             None,
             None,
@@ -108,8 +124,8 @@ async fn test_multi_mint_wallet_mint() {
         .unwrap();
 
     // Poll for quote to be paid (like a real wallet would)
-    let mut quote_status = multi_mint_wallet
-        .refresh_mint_quote(&mint_url, &mint_quote.id)
+    let mut quote_status = wallet
+        .refresh_mint_quote_status(&mint_quote.id)
         .await
         .unwrap();
 
@@ -124,15 +140,20 @@ async fn test_multi_mint_wallet_mint() {
             );
         }
         tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
-        quote_status = multi_mint_wallet
-            .refresh_mint_quote(&mint_url, &mint_quote.id)
+        quote_status = wallet
+            .refresh_mint_quote_status(&mint_quote.id)
             .await
             .unwrap();
     }
+    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
+    let _ = wallet
+        .refresh_mint_quote_status(&mint_quote.id)
+        .await
+        .unwrap();
 
     // Call mint() directly (quote should be Paid at this point)
-    let proofs = multi_mint_wallet
-        .mint(&mint_url, &mint_quote.id, SplitTarget::default(), None)
+    let proofs = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
         .await
         .unwrap();
 
@@ -140,7 +161,11 @@ async fn test_multi_mint_wallet_mint() {
     assert_eq!(minted_amount, 100.into(), "Should mint exactly 100 sats");
 
     // Verify balance
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
+    let balances = wallet_repository.total_balance().await.unwrap();
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance, 100.into(), "Total balance should be 100 sats");
 }
 
@@ -151,24 +176,43 @@ async fn test_multi_mint_wallet_mint() {
 /// 2. Call melt() without specifying mint (auto-selection)
 /// 3. Verify payment is made
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_melt_auto_select() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_melt_auto_select() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Fund the wallet
-    let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    let funded_amount = fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
     assert_eq!(funded_amount, 100.into());
 
     // Create an invoice to pay
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
 
-    // Use melt() with auto-selection (no specific mint specified)
-    let melt_result = multi_mint_wallet.melt(&invoice, None, None).await.unwrap();
+    // Get wallet and call melt
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
+    let melt_quote = wallet
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
+        .await
+        .unwrap();
+    let melt_result = wallet
+        .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
+        .await
+        .unwrap()
+        .confirm()
+        .await
+        .unwrap();
 
     assert_eq!(
         melt_result.state(),
@@ -177,37 +221,44 @@ async fn test_multi_mint_wallet_melt_auto_select() {
     );
     assert_eq!(melt_result.amount(), 50.into(), "Should melt 50 sats");
 
-    // Verify balance decreased
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
+    // Verify balance
+    let balances = wallet_repository.total_balance().await.unwrap();
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert!(
         balance < 100.into(),
         "Balance should be less than 100 after melt"
     );
 }
 
-/// Test the receive() function on MultiMintWallet
+/// Test the receive() function on WalletRepository
 ///
 /// This test verifies:
 /// 1. Create a token from a wallet
-/// 2. Receive the token in a different MultiMintWallet
+/// 2. Receive the token in a different WalletRepository
 /// 3. Verify the token value is received
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_receive() {
+async fn test_wallet_repository_receive() {
     // Create sender wallet and fund it
-    let sender_wallet = create_test_multi_mint_wallet().await;
+    let sender_repo = create_test_wallet_repository().await;
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    sender_wallet
-        .add_mint(mint_url.clone())
+    sender_repo
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
-    let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
+    let funded_amount = fund_wallet_repository(&sender_repo, &mint_url, 100.into()).await;
     assert_eq!(funded_amount, 100.into());
 
     // Create a token to send
-    let send_options = SendOptions::default();
+    let sender_wallet = sender_repo
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
     let prepared_send = sender_wallet
-        .prepare_send(mint_url.clone(), 50.into(), send_options)
+        .prepare_send(50.into(), SendOptions::default())
         .await
         .unwrap();
 
@@ -215,17 +266,20 @@ async fn test_multi_mint_wallet_receive() {
     let token_string = token.to_string();
 
     // Create receiver wallet
-    let receiver_wallet = create_test_multi_mint_wallet().await;
+    let receiver_repo = create_test_wallet_repository().await;
     // Add the same mint as trusted
-    receiver_wallet
-        .add_mint(mint_url.clone())
+    receiver_repo
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Receive the token
-    let receive_options = MultiMintReceiveOptions::default();
+    let receiver_wallet = receiver_repo
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
     let received_amount = receiver_wallet
-        .receive(&token_string, receive_options)
+        .receive(&token_string, ReceiveOptions::default())
         .await
         .unwrap();
 
@@ -237,14 +291,22 @@ async fn test_multi_mint_wallet_receive() {
     );
 
     // Verify receiver balance
-    let receiver_balance = receiver_wallet.total_balance().await.unwrap();
+    let receiver_balances = receiver_repo.total_balance().await.unwrap();
+    let receiver_balance = receiver_balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert!(
         receiver_balance > Amount::ZERO,
         "Receiver should have balance"
     );
 
     // Verify sender balance decreased
-    let sender_balance = sender_wallet.total_balance().await.unwrap();
+    let sender_balances = sender_repo.total_balance().await.unwrap();
+    let sender_balance = sender_balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert!(
         sender_balance < 100.into(),
         "Sender balance should be less than 100 after send"
@@ -258,22 +320,25 @@ async fn test_multi_mint_wallet_receive() {
 /// 2. Receive with a wallet that doesn't have the mint added
 /// 3. With allow_untrusted=true, the mint should be added automatically
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_receive_untrusted() {
+async fn test_wallet_repository_receive_untrusted() {
     // Create sender wallet and fund it
-    let sender_wallet = create_test_multi_mint_wallet().await;
+    let sender_repo = create_test_wallet_repository().await;
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    sender_wallet
-        .add_mint(mint_url.clone())
+    sender_repo
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
-    let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
+    let funded_amount = fund_wallet_repository(&sender_repo, &mint_url, 100.into()).await;
     assert_eq!(funded_amount, 100.into());
 
     // Create a token to send
-    let send_options = SendOptions::default();
+    let sender_wallet = sender_repo
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
     let prepared_send = sender_wallet
-        .prepare_send(mint_url.clone(), 50.into(), send_options)
+        .prepare_send(50.into(), SendOptions::default())
         .await
         .unwrap();
 
@@ -281,28 +346,31 @@ async fn test_multi_mint_wallet_receive_untrusted() {
     let token_string = token.to_string();
 
     // Create receiver wallet WITHOUT adding the mint
-    let receiver_wallet = create_test_multi_mint_wallet().await;
+    let receiver_repo = create_test_wallet_repository().await;
 
-    // First, verify that receiving without allow_untrusted fails
-    let receive_options = MultiMintReceiveOptions::default();
-    let result = receiver_wallet
-        .receive(&token_string, receive_options)
-        .await;
-    assert!(result.is_err(), "Should fail without allow_untrusted");
+    // Add the mint first, then receive (untrusted receive would require the
+    // WalletRepository to auto-add mints, which it doesn't support directly)
+    receiver_repo
+        .add_wallet(mint_url.clone())
+        .await
+        .expect("failed to add mint");
 
-    // Now receive with allow_untrusted=true
-    let receive_options = MultiMintReceiveOptions::default().allow_untrusted(true);
+    // Now receive
+    let receiver_wallet = receiver_repo
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
     let received_amount = receiver_wallet
-        .receive(&token_string, receive_options)
+        .receive(&token_string, ReceiveOptions::default())
         .await
         .unwrap();
 
     assert!(received_amount > Amount::ZERO, "Should receive some amount");
 
-    // Verify the mint was added to the wallet
+    // Verify the mint is in the wallet
     assert!(
-        receiver_wallet.has_mint(&mint_url).await,
-        "Mint should be added to wallet"
+        receiver_repo.has_mint(&mint_url).await,
+        "Mint should be in wallet"
     );
 }
 
@@ -314,23 +382,26 @@ async fn test_multi_mint_wallet_receive_untrusted() {
 /// 3. Confirm the send and get a token
 /// 4. Verify the token is valid
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_prepare_send_happy_path() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_prepare_send_happy_path() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Fund the wallet
-    let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    let funded_amount = fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
     assert_eq!(funded_amount, 100.into());
 
     // Prepare send
-    let send_options = SendOptions::default();
-    let prepared_send = multi_mint_wallet
-        .prepare_send(mint_url.clone(), 50.into(), send_options)
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
+    let prepared_send = wallet
+        .prepare_send(50.into(), SendOptions::default())
         .await
         .unwrap();
 
@@ -344,14 +415,18 @@ async fn test_multi_mint_wallet_prepare_send_happy_path() {
     assert_eq!(token_mint_url, mint_url, "Token mint URL should match");
 
     // Get token data to verify value
-    let token_data = multi_mint_wallet
+    let token_data = wallet_repository
         .get_token_data(&parsed_token)
         .await
         .unwrap();
     assert_eq!(token_data.value, 50.into(), "Token value should be 50 sats");
 
     // Verify wallet balance decreased
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
+    let balances = wallet_repository.total_balance().await.unwrap();
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance, 50.into(), "Remaining balance should be 50 sats");
 }
 
@@ -362,30 +437,40 @@ async fn test_multi_mint_wallet_prepare_send_happy_path() {
 /// 2. After minting, balance is updated
 /// 3. get_balances() returns per-mint breakdown
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_get_balances() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_get_balances() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Check initial balances
-    let balances = multi_mint_wallet.get_balances().await.unwrap();
-    let initial_balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
+    let balances = wallet_repository.get_balances().await.unwrap();
+    let initial_balance = balances
+        .get(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat))
+        .cloned()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(initial_balance, Amount::ZERO, "Initial balance should be 0");
 
     // Fund the wallet
-    fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
 
     // Check balances again
-    let balances = multi_mint_wallet.get_balances().await.unwrap();
-    let balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
+    let balances = wallet_repository.get_balances().await.unwrap();
+    let balance = balances
+        .get(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat))
+        .cloned()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance, 100.into(), "Balance should be 100 sats");
 
     // Verify total_balance matches
-    let total = multi_mint_wallet.total_balance().await.unwrap();
+    let total_balances = wallet_repository.total_balance().await.unwrap();
+    let total = total_balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(total, 100.into(), "Total balance should match");
 }
 
@@ -395,26 +480,32 @@ async fn test_multi_mint_wallet_get_balances() {
 /// 1. Empty wallet has no proofs
 /// 2. After minting, proofs are listed correctly
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_list_proofs() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_list_proofs() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Check initial proofs
-    let proofs = multi_mint_wallet.list_proofs().await.unwrap();
-    let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
+    let proofs = wallet_repository.list_proofs().await.unwrap();
+    let mint_proofs = proofs
+        .get(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat))
+        .cloned()
+        .unwrap_or_default();
     assert!(mint_proofs.is_empty(), "Should have no proofs initially");
 
     // Fund the wallet
-    fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
 
     // Check proofs again
-    let proofs = multi_mint_wallet.list_proofs().await.unwrap();
-    let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
+    let proofs = wallet_repository.list_proofs().await.unwrap();
+    let mint_proofs = proofs
+        .get(&WalletKey::new(mint_url.clone(), CurrencyUnit::Sat))
+        .cloned()
+        .unwrap_or_default();
     assert!(!mint_proofs.is_empty(), "Should have proofs after minting");
 
     // Verify proof total matches balance
@@ -422,52 +513,62 @@ async fn test_multi_mint_wallet_list_proofs() {
     assert_eq!(proof_total, 100.into(), "Proof total should be 100 sats");
 }
 
-/// Test mint management functions (add_mint, remove_mint, has_mint)
+/// Test mint management functions (add_mint, remove_wallet, has_mint)
 ///
 /// This test verifies:
 /// 1. has_mint returns false for unknown mints
 /// 2. add_mint adds the mint
 /// 3. has_mint returns true after adding
-/// 4. remove_mint removes the mint
+/// 4. remove_wallet removes the mint
 /// 5. has_mint returns false after removal
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_mint_management() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_mint_management() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
 
     // Initially mint should not be in wallet
     assert!(
-        !multi_mint_wallet.has_mint(&mint_url).await,
+        !wallet_repository.has_mint(&mint_url).await,
         "Mint should not be in wallet initially"
     );
 
     // Add the mint
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Now mint should be in wallet
     assert!(
-        multi_mint_wallet.has_mint(&mint_url).await,
+        wallet_repository.has_mint(&mint_url).await,
         "Mint should be in wallet after adding"
     );
 
     // Get wallets should include this mint
-    let wallets = multi_mint_wallet.get_wallets().await;
+    let wallets = wallet_repository.get_wallets().await;
     assert!(!wallets.is_empty(), "Should have at least one wallet");
 
     // Get specific wallet
-    let wallet = multi_mint_wallet.get_wallet(&mint_url).await;
-    assert!(wallet.is_some(), "Should be able to get wallet for mint");
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await;
+    assert!(wallet.is_ok(), "Should be able to get wallet for mint");
 
-    // Remove the mint
-    multi_mint_wallet.remove_mint(&mint_url).await;
+    // Get wallets for this mint
+    let mint_wallets = wallet_repository.get_wallets_for_mint(&mint_url).await;
+
+    // Remove all wallets for the mint
+    for wallet in mint_wallets {
+        wallet_repository
+            .remove_wallet(mint_url.clone(), wallet.unit.clone())
+            .await
+            .unwrap();
+    }
 
     // Now mint should not be in wallet
     assert!(
-        !multi_mint_wallet.has_mint(&mint_url).await,
+        !wallet_repository.has_mint(&mint_url).await,
         "Mint should not be in wallet after removal"
     );
 }
@@ -480,20 +581,24 @@ async fn test_multi_mint_wallet_mint_management() {
 /// 3. Poll until quote is paid (like a real wallet would)
 /// 4. check_all_mint_quotes() processes paid quotes
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_check_all_mint_quotes() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_check_all_mint_quotes() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
+
     // Create a mint quote
-    let mint_quote = multi_mint_wallet
+    let mint_quote = wallet
         .mint_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(100.into()),
             None,
             None,
@@ -508,29 +613,36 @@ async fn test_multi_mint_wallet_check_all_mint_quotes() {
         .unwrap();
 
     // Poll for quote to be paid (like a real wallet would)
-    let mut quote_status = multi_mint_wallet
-        .refresh_mint_quote(&mint_url, &mint_quote.id)
+    let mut quote_status = wallet
+        .refresh_mint_quote_status(&mint_quote.id)
         .await
         .unwrap();
 
     let timeout = tokio::time::Duration::from_secs(30);
     let start = tokio::time::Instant::now();
-    while quote_status.state != MintQuoteState::Paid {
+    while quote_status.state != MintQuoteState::Paid && quote_status.state != MintQuoteState::Issued
+    {
         if start.elapsed() > timeout {
             panic!(
                 "Timeout waiting for quote to be paid, state: {:?}",
                 quote_status.state
             );
         }
-        quote_status = multi_mint_wallet
-            .refresh_mint_quote(&mint_url, &mint_quote.id)
+        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
+        quote_status = wallet
+            .refresh_mint_quote_status(&mint_quote.id)
             .await
             .unwrap();
     }
+    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
+    let _ = wallet
+        .refresh_mint_quote_status(&mint_quote.id)
+        .await
+        .unwrap();
 
     // Check all mint quotes - this should find the paid quote and mint
-    let minted_amount = multi_mint_wallet
-        .mint_unissued_quotes(Some(mint_url.clone()))
+    let minted_amount = wallet_repository
+        .check_all_mint_quotes(Some(mint_url.clone()))
         .await
         .unwrap();
 
@@ -541,7 +653,11 @@ async fn test_multi_mint_wallet_check_all_mint_quotes() {
     );
 
     // Verify balance
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
+    let balances = wallet_repository.total_balance().await.unwrap();
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance, 100.into(), "Balance should be 100 sats");
 }
 
@@ -552,44 +668,58 @@ async fn test_multi_mint_wallet_check_all_mint_quotes() {
 /// 2. Create a new wallet with the same seed
 /// 3. Call restore() to recover the proofs
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_restore() {
+async fn test_wallet_repository_restore() {
     let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
 
     // Create first wallet and fund it
     {
         let localstore = Arc::new(memory::empty().await.unwrap());
-        let wallet1 = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
+        let wallet1 = WalletRepositoryBuilder::new()
+            .localstore(localstore)
+            .seed(seed)
+            .build()
             .await
             .expect("failed to create wallet");
 
         wallet1
-            .add_mint(mint_url.clone())
+            .add_wallet(mint_url.clone())
             .await
             .expect("failed to add mint");
 
-        let funded = fund_multi_mint_wallet(&wallet1, &mint_url, 100.into()).await;
+        let funded = fund_wallet_repository(&wallet1, &mint_url, 100.into()).await;
         assert_eq!(funded, 100.into());
     }
     // wallet1 goes out of scope
 
     // Create second wallet with same seed but fresh storage
     let localstore2 = Arc::new(memory::empty().await.unwrap());
-    let wallet2 = MultiMintWallet::new(localstore2, seed, CurrencyUnit::Sat)
+    let wallet2 = WalletRepositoryBuilder::new()
+        .localstore(localstore2)
+        .seed(seed)
+        .build()
         .await
         .expect("failed to create wallet");
 
     wallet2
-        .add_mint(mint_url.clone())
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Initially should have no balance
-    let balance_before = wallet2.total_balance().await.unwrap();
+    let balances_before = wallet2.total_balance().await.unwrap();
+    let balance_before = balances_before
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     assert_eq!(balance_before, Amount::ZERO, "Should start with no balance");
 
-    // Restore from mint
-    let restored = wallet2.restore(&mint_url).await.unwrap();
+    // Restore from mint using the individual wallet
+    let wallet = wallet2
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
+    let restored = wallet.restore().await.unwrap();
     assert_eq!(restored.unspent, 100.into(), "Should restore 100 sats");
 }
 
@@ -598,33 +728,45 @@ async fn test_multi_mint_wallet_restore() {
 /// This test verifies:
 /// 1. Fund wallet
 /// 2. Create melt quote at specific mint
-/// 3. Execute melt_with_mint()
+/// 3. Execute melt()
 /// 4. Verify payment succeeded
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_melt_with_mint() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_melt_with_mint() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Fund the wallet
-    fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
 
     // Create an invoice to pay
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
 
-    // Create melt quote at specific mint
-    let melt_quote = multi_mint_wallet
-        .melt_quote(&mint_url, PaymentMethod::BOLT11, invoice, None, None)
+    // Get wallet for operations
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
         .await
         .unwrap();
 
-    // Execute melt with specific mint
-    let melt_result = multi_mint_wallet
-        .melt_with_mint(&mint_url, &melt_quote.id)
+    // Create melt quote at specific mint
+    let melt_quote = wallet
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
+        .await
+        .unwrap();
+    let melt_result = wallet
+        .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
+        .await
+        .unwrap()
+        .confirm()
         .await
         .unwrap();
 
@@ -635,8 +777,8 @@ async fn test_multi_mint_wallet_melt_with_mint() {
     );
 
     // Check melt quote status
-    let quote_status = multi_mint_wallet
-        .check_melt_quote(&mint_url, &melt_quote.id)
+    let quote_status = wallet
+        .check_melt_quote_status(&melt_quote.id)
         .await
         .unwrap();
 
@@ -647,19 +789,6 @@ async fn test_multi_mint_wallet_melt_with_mint() {
     );
 }
 
-/// Test unit() function returns correct currency unit
-#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_unit() {
-    let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
-    let localstore = Arc::new(memory::empty().await.unwrap());
-
-    let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
-        .await
-        .expect("failed to create wallet");
-
-    assert_eq!(wallet.unit(), &CurrencyUnit::Sat, "Unit should be Sat");
-}
-
 /// Test list_transactions() function
 ///
 /// This test verifies:
@@ -667,101 +796,54 @@ async fn test_multi_mint_wallet_unit() {
 /// 2. After minting, transaction is recorded
 /// 3. After melting, transaction is recorded
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_list_transactions() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
+async fn test_wallet_repository_list_transactions() {
+    let wallet_repository = create_test_wallet_repository().await;
 
     let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
+    wallet_repository
+        .add_wallet(mint_url.clone())
         .await
         .expect("failed to add mint");
 
     // Fund the wallet (this creates a mint transaction)
-    fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
+    fund_wallet_repository(&wallet_repository, &mint_url, 100.into()).await;
 
     // List all transactions
-    let transactions = multi_mint_wallet.list_transactions(None).await.unwrap();
+    let transactions = wallet_repository.list_transactions(None).await.unwrap();
     assert!(
         !transactions.is_empty(),
         "Should have at least one transaction after minting"
     );
 
+    // Get wallet for melt operations
+    let wallet = wallet_repository
+        .get_wallet(&mint_url, &CurrencyUnit::Sat)
+        .await
+        .unwrap();
+
     // Create an invoice and melt (this creates a melt transaction)
     let invoice = create_invoice_for_env(Some(50)).await.unwrap();
-    let melt_quote = multi_mint_wallet
-        .melt_quote(&mint_url, PaymentMethod::BOLT11, invoice, None, None)
+    let melt_quote = wallet
+        .melt_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            invoice.to_string(),
+            None,
+            None,
+        )
         .await
         .unwrap();
-    multi_mint_wallet
-        .melt_with_mint(&mint_url, &melt_quote.id)
+    wallet
+        .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
+        .await
+        .unwrap()
+        .confirm()
         .await
         .unwrap();
 
     // List transactions again
-    let transactions_after = multi_mint_wallet.list_transactions(None).await.unwrap();
+    let transactions_after = wallet_repository.list_transactions(None).await.unwrap();
     assert!(
         transactions_after.len() > transactions.len(),
         "Should have more transactions after melt"
     );
 }
-
-/// Test send revocation via MultiMintWallet
-///
-/// This test verifies:
-/// 1. Create and confirm a send
-/// 2. Verify it appears in pending sends
-/// 3. Verify status is "not claimed"
-/// 4. Revoke the send
-/// 5. Verify balance is restored and pending send is gone
-#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
-async fn test_multi_mint_wallet_revoke_send() {
-    let multi_mint_wallet = create_test_multi_mint_wallet().await;
-
-    let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
-    multi_mint_wallet
-        .add_mint(mint_url.clone())
-        .await
-        .expect("failed to add mint");
-
-    // Fund the wallet
-    fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
-
-    // Create a send
-    let send_options = SendOptions::default();
-    let prepared_send = multi_mint_wallet
-        .prepare_send(mint_url.clone(), 50.into(), send_options)
-        .await
-        .unwrap();
-
-    let operation_id = prepared_send.operation_id();
-    let _token = prepared_send.confirm(None).await.unwrap();
-
-    // Verify it appears in pending sends
-    let pending = multi_mint_wallet.get_pending_sends().await.unwrap();
-    assert_eq!(pending.len(), 1, "Should have 1 pending send");
-    assert_eq!(pending[0].0, mint_url, "Mint URL should match");
-    assert_eq!(pending[0].1, operation_id, "Operation ID should match");
-
-    // Verify status
-    let claimed = multi_mint_wallet
-        .check_send_status(mint_url.clone(), operation_id)
-        .await
-        .unwrap();
-    assert!(!claimed, "Token should not be claimed yet");
-
-    // Revoke the send
-    let restored_amount = multi_mint_wallet
-        .revoke_send(mint_url.clone(), operation_id)
-        .await
-        .unwrap();
-
-    assert_eq!(restored_amount, 50.into(), "Should restore 50 sats");
-
-    // Verify pending send is gone
-    let pending_after = multi_mint_wallet.get_pending_sends().await.unwrap();
-    assert!(pending_after.is_empty(), "Should have no pending sends");
-
-    // Verify balance is back to 100
-    let balance = multi_mint_wallet.total_balance().await.unwrap();
-    assert_eq!(balance, 100.into(), "Balance should be fully restored");
-}

+ 12 - 9
crates/cdk/examples/configure_wallet.rs

@@ -11,8 +11,7 @@ use std::time::Duration;
 
 use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::multi_mint_wallet::WalletConfig;
-use cdk::wallet::{MultiMintWallet, WalletBuilder};
+use cdk::wallet::{WalletBuilder, WalletConfig, WalletRepositoryBuilder};
 use cdk_sqlite::wallet::memory;
 use rand::random;
 
@@ -45,12 +44,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Updated wallet TTL to 5 minutes");
 
     // ==========================================
-    // 2. Configure MultiMintWallet
+    // 2. Configure WalletRepository
     // ==========================================
-    println!("\n=== MultiMintWallet Configuration ===");
+    println!("\n=== WalletRepository Configuration ===");
 
-    // Create the MultiMintWallet
-    let multi_wallet = MultiMintWallet::new(localstore.clone(), seed, unit.clone()).await?;
+    // Create the WalletRepository
+    let multi_wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore.clone())
+        .seed(seed)
+        .build()
+        .await?;
 
     // Define configuration for a new mint
     // This config uses a very short 1-minute TTL
@@ -60,7 +63,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     // Add the mint with the custom configuration
     multi_wallet
-        .add_mint_with_config(mint_url_2.clone(), config.clone())
+        .add_wallet_with_config(mint_url_2.clone(), Some(config.clone()))
         .await?;
     println!("Added mint {} with 1 minute TTL", mint_url_2);
 
@@ -68,9 +71,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Let's disable auto-refresh (set to None) for the first mint
     let no_refresh_config = WalletConfig::new().with_metadata_cache_ttl(None); // Never expire
 
-    multi_wallet.add_mint(mint_url.clone()).await?; // Add first mint with default settings
+    multi_wallet.add_wallet(mint_url.clone()).await?; // Add first mint with default settings
     multi_wallet
-        .set_mint_config(mint_url.clone(), no_refresh_config)
+        .set_mint_config(mint_url.clone(), unit.clone(), no_refresh_config)
         .await?;
     println!("Updated mint {} to never expire metadata cache", mint_url);
 

+ 60 - 34
crates/cdk/examples/multi-mint-wallet.rs

@@ -8,18 +8,17 @@ use std::time::Duration;
 use bip39::Mnemonic;
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
-use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::nut00::{KnownMethod, ProofsMethods};
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::{MultiMintReceiveOptions, SendOptions};
+use cdk::wallet::{ReceiveOptions, SendOptions, WalletRepositoryBuilder};
 use cdk::Amount;
 use cdk_fake_wallet::create_fake_invoice;
 use cdk_sqlite::wallet::memory;
 
-/// This example demonstrates the MultiMintWallet API for managing multiple mints.
+/// This example demonstrates the WalletRepository API for managing multiple mints.
 ///
 /// It shows:
-/// - Creating a MultiMintWallet
+/// - Creating a WalletRepository
 /// - Adding a mint
 /// - Minting proofs
 /// - Sending tokens
@@ -37,15 +36,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let seed = mnemonic.to_seed_normalized("");
     println!("Generated mnemonic (save this!): {}", mnemonic);
 
-    // Create the MultiMintWallet
+    // Create the WalletRepository
     let localstore = Arc::new(memory::empty().await?);
-    let wallet = MultiMintWallet::new(localstore, seed, unit.clone()).await?;
-    println!("\nCreated MultiMintWallet");
+    let wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore)
+        .seed(seed)
+        .build()
+        .await?;
+    println!("\nCreated WalletRepository");
 
     // Add a mint to the wallet
-    wallet.add_mint(mint_url.clone()).await?;
+    wallet.add_wallet(mint_url.clone()).await?;
     println!("Added mint: {}", mint_url);
 
+    // Get the wallet for this mint
+    let mint_wallet = wallet
+        .create_wallet(mint_url.clone(), unit.clone(), None)
+        .await?;
+
     // ========================================
     // MINT: Create proofs from Lightning invoice
     // ========================================
@@ -53,10 +61,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- MINT ---");
     println!("Creating mint quote for {} sats...", mint_amount);
 
-    let mint_quote = wallet
+    let mint_quote = mint_wallet
         .mint_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(mint_amount),
             None,
             None,
@@ -66,10 +73,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     // Wait for quote to be paid and mint proofs
     // With the fake mint, this happens automatically
-    let proofs = wallet
-        .wait_for_mint_quote(
-            &mint_url,
-            &mint_quote.id,
+    let proofs = mint_wallet
+        .wait_and_mint_quote(
+            mint_quote.clone(),
             SplitTarget::default(),
             None,
             Duration::from_secs(30),
@@ -80,7 +86,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Minted {} sats", minted_amount);
 
     // Check balance
-    let balance = wallet.total_balance().await?;
+    let balances = wallet.total_balance().await?;
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Total balance: {} sats", balance);
 
     // ========================================
@@ -90,14 +100,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- SEND ---");
     println!("Preparing to send {} sats...", send_amount);
 
-    let prepared_send = wallet
-        .prepare_send(mint_url.clone(), send_amount, SendOptions::default())
+    let prepared_send = mint_wallet
+        .prepare_send(send_amount, SendOptions::default())
         .await?;
     let token = prepared_send.confirm(None).await?;
     println!("Token created:\n{}", token);
 
     // Check balance after send
-    let balance = wallet.total_balance().await?;
+    let balances = wallet.total_balance().await?;
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Balance after send: {} sats", balance);
 
     // ========================================
@@ -108,19 +122,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Create a second wallet to receive the token
     let receiver_seed = Mnemonic::generate(12)?.to_seed_normalized("");
     let receiver_store = Arc::new(memory::empty().await?);
-    let receiver_wallet = MultiMintWallet::new(receiver_store, receiver_seed, unit).await?;
+    let receiver_wallet = WalletRepositoryBuilder::new()
+        .localstore(receiver_store)
+        .seed(receiver_seed)
+        .build()
+        .await?;
 
     // Add the mint (or use allow_untrusted)
-    receiver_wallet.add_mint(mint_url.clone()).await?;
+    receiver_wallet.add_wallet(mint_url.clone()).await?;
+    let receiver_mint_wallet = receiver_wallet
+        .create_wallet(mint_url.clone(), unit, None)
+        .await?;
 
     // Receive the token
-    let received = receiver_wallet
-        .receive(&token.to_string(), MultiMintReceiveOptions::default())
+    let received = receiver_mint_wallet
+        .receive(&token.to_string(), ReceiveOptions::default())
         .await?;
     println!("Receiver got {} sats", received);
 
     // Check receiver balance
-    let receiver_balance = receiver_wallet.total_balance().await?;
+    let receiver_balances = receiver_wallet.total_balance().await?;
+    let receiver_balance = receiver_balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Receiver balance: {} sats", receiver_balance);
 
     // ========================================
@@ -135,10 +160,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Invoice: {}", invoice);
 
     // Create melt quote
-    let melt_quote = wallet
+    let melt_quote = mint_wallet
         .melt_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             invoice.to_string(),
             None,
             None,
@@ -150,8 +174,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     );
 
     // Prepare and execute melt
-    let prepared_melt = wallet
-        .prepare_melt(&mint_url, &melt_quote.id, HashMap::new())
+    let prepared_melt = mint_wallet
+        .prepare_melt(&melt_quote.id, HashMap::new())
         .await?;
     let melt_result = prepared_melt.confirm().await?;
     println!("Melt completed! State: {:?}", melt_result.state());
@@ -161,12 +185,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // ========================================
     println!("\n--- BALANCES ---");
 
-    let total = wallet.total_balance().await?;
-    println!("Total balance: {} sats", total);
+    let total_balances = wallet.total_balance().await?;
+    for (unit, amount) in &total_balances {
+        println!("  {}: {} sats", unit, amount);
+    }
 
     let per_mint = wallet.get_balances().await?;
-    for (url, amount) in per_mint {
-        println!("  {}: {} sats", url, amount);
+    for (key, amount) in per_mint {
+        println!("  {} ({}): {} sats", key.mint_url, key.unit, amount);
     }
 
     // List all mints

+ 17 - 12
crates/cdk/examples/multimint-npubcash.rs

@@ -17,7 +17,7 @@ use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::wallet::WalletRepositoryBuilder;
 use cdk::StreamExt;
 use cdk_sqlite::wallet::memory;
 use nostr_sdk::ToBech32;
@@ -50,25 +50,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     };
 
     let localstore = memory::empty().await?;
-    let wallet = MultiMintWallet::new(Arc::new(localstore), seed, CurrencyUnit::Sat).await?;
+    let wallet_repository = WalletRepositoryBuilder::new()
+        .localstore(Arc::new(localstore))
+        .seed(seed)
+        .build()
+        .await?;
 
     let mint_url_1: MintUrl = MINT_URL_1.parse()?;
     let mint_url_2: MintUrl = MINT_URL_2.parse()?;
 
-    wallet.add_mint(mint_url_1.clone()).await?;
-    wallet.add_mint(mint_url_2.clone()).await?;
+    wallet_repository.add_wallet(mint_url_1.clone()).await?;
+    wallet_repository.add_wallet(mint_url_2.clone()).await?;
     println!("   Added mints: {}, {}\n", mint_url_1, mint_url_2);
 
     // -------------------------------------------------------------------------
     // Step 2: Enable NpubCash on mint 1
     // -------------------------------------------------------------------------
     println!("Step 2: Enabling NpubCash on mint 1...\n");
-
-    wallet
-        .enable_npubcash(mint_url_1.clone(), NPUBCASH_URL.to_string())
+    let wallet = wallet_repository
+        .get_wallet(&mint_url_1.clone(), &CurrencyUnit::Sat)
         .await?;
+    wallet.enable_npubcash(NPUBCASH_URL.to_string()).await?;
 
-    let keys = wallet.get_npubcash_keys().await?;
+    let keys = wallet.get_npubcash_keys().unwrap();
     let npub = keys.public_key().to_bech32()?;
     let display_url = NPUBCASH_URL.trim_start_matches("https://");
 
@@ -97,10 +101,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Step 4: Switch to mint 2 and receive payment
     // -------------------------------------------------------------------------
     println!("Step 4: Switching to mint 2 and receiving payment...\n");
-
-    wallet
-        .enable_npubcash(mint_url_2.clone(), NPUBCASH_URL.to_string())
+    let wallet = wallet_repository
+        .get_wallet(&mint_url_1.clone(), &CurrencyUnit::Sat)
         .await?;
+
+    wallet.enable_npubcash(NPUBCASH_URL.to_string()).await?;
     println!("   Switched to mint: {}", mint_url_2);
 
     request_invoice(&npub, PAYMENT_AMOUNT_MSATS).await?;
@@ -117,7 +122,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // -------------------------------------------------------------------------
     println!("Step 5: Verifying balances...\n");
 
-    let balances = wallet.get_balances().await?;
+    let balances = wallet_repository.get_balances().await?;
     for (mint, balance) in &balances {
         println!("   {}: {} sats", mint, balance);
     }

+ 18 - 15
crates/cdk/examples/nostr_backup.rs

@@ -1,7 +1,7 @@
 //! # Nostr Mint Backup Example (NUT-XX)
 //!
 //! This example demonstrates how to backup and restore your mint list
-//! to/from Nostr relays using the MultiMintWallet.
+//! to/from Nostr relays using the WalletRepository.
 //!
 //! ## Features
 //!
@@ -24,9 +24,7 @@
 
 use std::sync::Arc;
 
-use cdk::nuts::CurrencyUnit;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::{BackupOptions, RestoreOptions};
+use cdk::wallet::{BackupOptions, RestoreOptions, WalletRepositoryBuilder};
 use cdk_sqlite::wallet::memory;
 use rand::random;
 
@@ -44,14 +42,15 @@ async fn main() -> anyhow::Result<()> {
     // In production, this would be derived from a BIP-39 mnemonic
     let seed: [u8; 64] = random();
 
-    // Currency unit for the wallet
-    let unit = CurrencyUnit::Sat;
-
     // Initialize the memory store for the first wallet
     let localstore = Arc::new(memory::empty().await?);
 
-    // Create a new MultiMintWallet
-    let wallet = MultiMintWallet::new(localstore.clone(), seed, unit.clone()).await?;
+    // Create a new WalletRepository
+    let wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore.clone())
+        .seed(seed)
+        .build()
+        .await?;
 
     // ============================================================================
     // Step 1: Add test mints to the wallet
@@ -67,14 +66,14 @@ async fn main() -> anyhow::Result<()> {
 
     for mint_url in &mints {
         println!("  Adding mint: {}", mint_url);
-        match wallet.add_mint(mint_url.parse()?).await {
-            Ok(()) => println!("    + Added successfully"),
+        match wallet.add_wallet(mint_url.parse()?).await {
+            Ok(_) => println!("    + Added successfully"),
             Err(e) => println!("    x Failed to add: {}", e),
         }
     }
 
     // Verify mints were added
-    let wallets = wallet.get_wallets().await;
+    let wallets: Vec<cdk::Wallet> = wallet.get_wallets().await;
     println!("\n  Wallet now contains {} mint(s):", wallets.len());
     for w in &wallets {
         println!("    - {}", w.mint_url);
@@ -129,10 +128,14 @@ async fn main() -> anyhow::Result<()> {
 
     // Create a fresh wallet with the same seed (simulating a new device)
     let new_localstore = Arc::new(memory::empty().await?);
-    let new_wallet = MultiMintWallet::new(new_localstore, seed, unit.clone()).await?;
+    let new_wallet = WalletRepositoryBuilder::new()
+        .localstore(new_localstore)
+        .seed(seed)
+        .build()
+        .await?;
 
     // Verify the new wallet is empty
-    let new_wallets = new_wallet.get_wallets().await;
+    let new_wallets: Vec<cdk::Wallet> = new_wallet.get_wallets().await;
     println!("  New wallet starts with {} mint(s)", new_wallets.len());
 
     // Derive keys on the new wallet - should be the same!
@@ -172,7 +175,7 @@ async fn main() -> anyhow::Result<()> {
     }
 
     // Verify the mints were restored
-    let restored_wallets = new_wallet.get_wallets().await;
+    let restored_wallets: Vec<cdk::Wallet> = new_wallet.get_wallets().await;
     println!(
         "\n  New wallet now contains {} mint(s):",
         restored_wallets.len()

+ 30 - 14
crates/cdk/examples/payment_request.rs

@@ -1,7 +1,7 @@
 //! # Payment Request Example (NUT-18)
 //!
 //! This example demonstrates how to create and receive payments using NUT-18
-//! payment requests with the MultiMintWallet. It shows both HTTP and Nostr
+//! payment requests with the WalletRepository. It shows both HTTP and Nostr
 //! transport options.
 //!
 //! ## Payment Request Flow
@@ -26,11 +26,11 @@
 use std::sync::Arc;
 use std::time::Duration;
 
-use anyhow::anyhow;
 use cdk::amount::SplitTarget;
+use cdk::nuts::nut00::KnownMethod;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
 use cdk::wallet::payment_request::CreateRequestParams;
+use cdk::wallet::WalletRepositoryBuilder;
 use cdk_sqlite::wallet::memory;
 use rand::random;
 
@@ -50,22 +50,34 @@ async fn main() -> anyhow::Result<()> {
     // Initialize the memory store
     let localstore = Arc::new(memory::empty().await?);
 
-    // Create a new MultiMintWallet
-    let wallet = MultiMintWallet::new(localstore, seed, unit.clone()).await?;
+    // Create a new WalletRepository
+    let wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore)
+        .seed(seed)
+        .build()
+        .await?;
 
     // Add the mint to our wallet
-    wallet.add_mint(mint_url.parse()?).await?;
+    wallet.add_wallet(mint_url.parse()?).await?;
+
+    println!("Using mint: {}", mint_url);
 
-    println!("Step 1: Funding the wallet");
-    println!("---------------------------");
+    // ============================================================================
+    // Step 1: Create a payment request (as the receiver)
+    // ============================================================================
+    println!("\nStep 1: Creating payment request...");
 
-    // Get a wallet for our mint to create a mint quote
+    // We need to get the wallet for the specific mint to create a request
     let mint_wallet = wallet
-        .get_wallet(&mint_url.parse()?)
-        .await
-        .ok_or_else(|| anyhow!("Wallet not found for mint"))?;
+        .create_wallet(mint_url.parse()?, unit.clone(), None)
+        .await?;
     let mint_quote = mint_wallet
-        .mint_quote(PaymentMethod::BOLT11, Some(initial_amount), None, None)
+        .mint_quote(
+            PaymentMethod::Known(KnownMethod::Bolt11),
+            Some(initial_amount),
+            None,
+            None,
+        )
         .await?;
 
     println!(
@@ -85,7 +97,11 @@ async fn main() -> anyhow::Result<()> {
         )
         .await?;
 
-    let balance = wallet.total_balance().await?;
+    let balances = wallet.total_balance().await?;
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(cdk::Amount::ZERO);
     println!("Wallet funded with {} sats\n", balance);
 
     // ============================================================================

+ 58 - 39
crates/cdk/examples/revoke_send.rs

@@ -7,9 +7,9 @@ use std::time::Duration;
 use bip39::Mnemonic;
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
+use cdk::nuts::nut00::KnownMethod;
 use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::SendOptions;
+use cdk::wallet::{ReceiveOptions, SendOptions, WalletRepositoryBuilder};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 
@@ -32,15 +32,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let seed = mnemonic.to_seed_normalized("");
     println!("Generated mnemonic: {}", mnemonic);
 
-    // Create the MultiMintWallet
+    // Create the WalletRepository
     let localstore = Arc::new(memory::empty().await?);
-    let wallet = MultiMintWallet::new(localstore, seed, unit.clone()).await?;
-    println!("Created MultiMintWallet");
+    let wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore)
+        .seed(seed)
+        .build()
+        .await?;
+    println!("Created WalletRepository");
 
     // Add a mint to the wallet
-    wallet.add_mint(mint_url.clone()).await?;
+    wallet.add_wallet(mint_url.clone()).await?;
     println!("Added mint: {}", mint_url);
 
+    // Get the wallet for this mint
+    let mint_wallet = wallet
+        .create_wallet(mint_url.clone(), unit.clone(), None)
+        .await?;
+
     // ========================================
     // 1. FUND: Mint some tokens to start
     // ========================================
@@ -48,10 +57,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- 1. FUNDING WALLET ---");
     println!("Minting {} sats...", mint_amount);
 
-    let mint_quote = wallet
+    let mint_quote = mint_wallet
         .mint_quote(
-            &mint_url,
-            PaymentMethod::BOLT11,
+            PaymentMethod::Known(KnownMethod::Bolt11),
             Some(mint_amount),
             None,
             None,
@@ -59,17 +67,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         .await?;
 
     // Wait for quote to be paid (automatic with fake mint)
-    let _proofs = wallet
-        .wait_for_mint_quote(
-            &mint_url,
-            &mint_quote.id,
+    let _proofs = mint_wallet
+        .wait_and_mint_quote(
+            mint_quote.clone(),
             SplitTarget::default(),
             None,
             Duration::from_secs(60),
         )
         .await?;
 
-    let balance = wallet.total_balance().await?;
+    let balances = wallet.total_balance().await?;
+    let balance = balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Wallet funded. Balance: {} sats", balance);
 
     // ========================================
@@ -80,8 +91,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Preparing to send {} sats...", send_amount);
 
     // Prepare and confirm the send
-    let prepared_send = wallet
-        .prepare_send(mint_url.clone(), send_amount, SendOptions::default())
+    let prepared_send = mint_wallet
+        .prepare_send(send_amount, SendOptions::default())
         .await?;
 
     let operation_id = prepared_send.operation_id();
@@ -90,7 +101,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Token created (Send Operation ID: {})", operation_id);
     println!("Token: {}", token);
 
-    let balance_after_send = wallet.total_balance().await?;
+    let balances_after_send = wallet.total_balance().await?;
+    let balance_after_send = balances_after_send
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Balance after send: {} sats", balance_after_send);
 
     // ========================================
@@ -99,17 +114,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- 3. INSPECTING STATUS ---");
 
     // Get all pending sends
-    let pending_sends = wallet.get_pending_sends().await?;
+    let pending_sends = mint_wallet.get_pending_sends().await?;
     println!("Pending sends count: {}", pending_sends.len());
 
-    for (mint, id) in &pending_sends {
-        println!("- Mint: {}, ID: {}", mint, id);
+    for id in &pending_sends {
+        println!("- ID: {}", id);
     }
 
     // Check specific status
-    let claimed = wallet
-        .check_send_status(mint_url.clone(), operation_id)
-        .await?;
+    let claimed = mint_wallet.check_send_status(operation_id).await?;
     println!("Is token claimed? {}", claimed);
 
     if !claimed {
@@ -125,7 +138,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- 4. REVOKING SEND ---");
     println!("Revoking operation {}...", operation_id);
 
-    let reclaimed_amount = wallet.revoke_send(mint_url.clone(), operation_id).await?;
+    let reclaimed_amount = mint_wallet.revoke_send(operation_id).await?;
     println!("Reclaimed {} sats", reclaimed_amount);
 
     // ========================================
@@ -134,11 +147,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("\n--- 5. VERIFYING STATE ---");
 
     // Check pending sends again
-    let pending_after = wallet.get_pending_sends().await?;
+    let pending_after = mint_wallet.get_pending_sends().await?;
     println!("Pending sends after revocation: {}", pending_after.len());
 
     // Check final balance
-    let final_balance = wallet.total_balance().await?;
+    let final_balances = wallet.total_balance().await?;
+    let final_balance = final_balances
+        .get(&CurrencyUnit::Sat)
+        .copied()
+        .unwrap_or(Amount::ZERO);
     println!("Final balance: {} sats", final_balance);
 
     if final_balance > balance_after_send {
@@ -160,8 +177,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Sending {} sats to be claimed...", send_amount_2);
 
     // Create a new send
-    let prepared_send_2 = wallet
-        .prepare_send(mint_url.clone(), send_amount_2, SendOptions::default())
+    let prepared_send_2 = mint_wallet
+        .prepare_send(send_amount_2, SendOptions::default())
         .await?;
     let operation_id_2 = prepared_send_2.operation_id();
     let token_2 = prepared_send_2.confirm(None).await?;
@@ -171,24 +188,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Creating receiver wallet...");
     let receiver_seed = Mnemonic::generate(12)?.to_seed_normalized("");
     let receiver_store = Arc::new(memory::empty().await?);
-    let receiver_wallet = MultiMintWallet::new(receiver_store, receiver_seed, unit).await?;
-    receiver_wallet.add_mint(mint_url.clone()).await?;
+    let receiver_wallet = WalletRepositoryBuilder::new()
+        .localstore(receiver_store)
+        .seed(receiver_seed)
+        .build()
+        .await?;
+    receiver_wallet.add_wallet(mint_url.clone()).await?;
+    let receiver_mint_wallet = receiver_wallet
+        .create_wallet(mint_url.clone(), unit, None)
+        .await?;
 
     // Receiver claims the token
     println!("Receiver claiming token...");
-    let received_amount = receiver_wallet
-        .receive(
-            &token_2.to_string(),
-            cdk::wallet::MultiMintReceiveOptions::default(),
-        )
+    let received_amount = receiver_mint_wallet
+        .receive(&token_2.to_string(), ReceiveOptions::default())
         .await?;
     println!("Receiver got {} sats", received_amount);
 
     // Check status from sender side
     println!("Checking status from sender...");
-    let claimed_2 = wallet
-        .check_send_status(mint_url.clone(), operation_id_2)
-        .await?;
+    let claimed_2 = mint_wallet.check_send_status(operation_id_2).await?;
     println!("Is token claimed? {}", claimed_2);
 
     if claimed_2 {
@@ -198,7 +217,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     }
 
     // Verify pending sends is empty
-    let pending_final = wallet.get_pending_sends().await?;
+    let pending_final = mint_wallet.get_pending_sends().await?;
     println!("Pending sends count: {}", pending_final.len());
 
     if pending_final.is_empty() {

+ 20 - 10
crates/cdk/examples/token-proofs.rs

@@ -1,7 +1,7 @@
-//! Example: Decoding a token and getting proofs using MultiMintWallet
+//! Example: Decoding a token and getting proofs using WalletRepository
 //!
 //! This example demonstrates how to:
-//! 1. Create a MultiMintWallet
+//! 1. Create a WalletRepository
 //! 2. Decode a cashu token
 //! 3. Use `get_token_data` to extract mint URL and proofs in one call
 //! 4. Alternatively, get keysets manually and extract proofs
@@ -10,8 +10,8 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::{CurrencyUnit, Token};
-use cdk::wallet::MultiMintWallet;
+use cdk::nuts::Token;
+use cdk::wallet::WalletRepositoryBuilder;
 use cdk_sqlite::wallet::memory;
 use rand::random;
 
@@ -23,11 +23,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Initialize the memory store
     let localstore = Arc::new(memory::empty().await?);
 
-    // Create a new multi-mint wallet for satoshis
-    let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat).await?;
+    // Create a new WalletRepository
+    let wallet = WalletRepositoryBuilder::new()
+        .localstore(localstore)
+        .seed(seed)
+        .build()
+        .await?;
 
     // Example: A cashu token string (in practice, this would come from user input)
-    let token = Token::from_str("cashuBo2FteB1odHRwczovL2Zha2UudGhlc2ltcGxla2lkLmRldmF1Y3NhdGF0gaJhaUgAlNWndMQKMmFwg6RhYRkIAGFzeEAwYjk0ZjU5ZjU0OTBkNTkzMzI4ZTIwNDllZTNlZmFjYjM5NzljZjU5NzA5ZTM3N2U5YzBmMDQyNDBmZTUyZTVhYWNYIQNGQCYyf1j996pS-LuP_7VsUE-uzRpAm-K4rZiDEFFc1GFko2FlWCBbuMkhvz39ytCzm7xPaY5vdTbqxlxTzXOsks_8S3sf1GFzWCBg22l0CXH5-QLcfJtUJZ2lfylNfC6_o9FTfKClLzthaGFyWCCP2nJ6Qzd8mwLa_85cu8TrwRIprElVgrhqJeoHJwXmSKRhYRkCAGFzeEBhNmMyODliMjMwMTdlMDhjYTFhOTc4ZjAwNGRiNjI4ZDk1NWI5ZTlmNjMwMjY0MjNjZDc4OGExNDBhOWJiYjgxYWNYIQPMXkT68L8Y0a6royMbkoUTbvxOUgsyDwvRZRNTvwUsWWFko2FlWCCj9BFXexBOrlUyUiY_1qEIEHvd1YphWA2l3YhdFwVRh2FzWCBTNgyGeXvGSFtvYKj3MnJCXA8qjI9fzZHFsIw-F_OAGmFyWCDRHiDbVysUuQZucifYx5zMvOKyVIz7zvcJcfd01FoI3KRhYQhhc3hAMWJjOWQ1MjE5ZTZhYzNjZmZhNTM0NTRkY2JjMzE1YzZjZjY5MmM5MDEzYTUzYTA1YzIzN2YwZTBiOTViZTkwMWFjWCEDXd5sxFgxYgUHctpLENYStcr50UtJ4QRojy0g7mkdvWRhZKNhZVggZzSifCUG692E2sW4L6DT_FuKwLZdUFoMnds3tQyMlAdhc1ggtIo0BS2-6arws5fJx_w0phOiCZZcHIFknlrDXSh3C0NhclggM2dDF0kQyuRoOqrOOMHFrmNnvtGiXWxuvqtD7HidR8I")?;
+    let token = Token::from_str("cashuBo2FteB1odHRwczovL2Zha2UudGhlc2ltcGxla2lkLmRldmF1Y3NhdGF0gaJhaUgAlNWndMQKMmFwg6RhYRkIAGFzeEAwYjk0ZjU5ZjU0OTBkNTkzMzI4ZTIwNDllZTNlZmFjYjM5NzljZjU5NzA5ZTM3N2U5YzBmMDQyNDBmZTUyZTVhYWNYIQNGQCYyf1j996pS-LuP_7VsUE-uzRpAm-K4rZiDEFFc1GFko2FlWCBbuMkhvz39ytCzm7xPaY5vdTbqxlxTzXOsks_8S3sf1GFzWCBg22l0CXH5-QLcfJtUJZ2lfylNfC6_o9FTfKClLzthaGFyWCCP2nJ6Qzd8mwLa_85cu8TrwRIprElVgrhqJeoHJwXmSKRhYRkCAGFzeEBhNmMyODliMjMwMTdlMDhjYTFhOTc4ZjAwNGRiNjI4ZDk1NWI5ZTlmNjMwMjY0MjNjZDc4OGExNDBhOWJiYjgxYWNYIQPMXkT68L8Y0a6royMbkoUTbvxOUgsyDwvRZRNTvwUsWWFko2FlWCCj9BFXexBOrlUyUiY_1qEIEHvd1YphWA2l3YhdFwVRh2FzWCBTNgyGeXvGSFtvYKj3MnJCXA8qjI9fzZHFsIw-F_OAGmFyWCDRHiDbVysUuQZucifYx5zMvOKyVIz7zvcJcfd01FoI3KRhYQhhc3hAMWJjOWQ1MjE5ZTZhYzNjZmZhNTM0NTRkY2JjMzE1YzZjZjY5NmM5MDEzYTUzYTA1YzIzN2YwZTBiOTViZTkwMWFjWCEDXd5sxFgxYgUHctpLENYStcr50UtJ4QRojy0g7mkdvWRhZKNhZVggZzSifCUG692E2sW4L6DT_FuKwLZdUFoMnds3tQyMlAdhc1ggtIo0BS2-6arws5fJx_w0phOiCZZcHIFknlrDXSh3C0NhclggM2dDF0kQyuRoOqrOOMHFrmNnvtGiXWxuvqtD7HidR8I")?;
 
     // Get the mint URL from the token
     let mint_url = token.mint_url()?;
@@ -43,7 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     }
 
     // Add the mint to our wallet so we can fetch keysets
-    wallet.add_mint(mint_url.clone()).await?;
+    wallet.add_wallet(mint_url.clone()).await?;
 
     // =========================================================================
     // Method 1: Use get_token_data() for a simple one-call approach
@@ -68,8 +72,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // =========================================================================
     println!("\n--- Using manual keyset lookup ---");
 
-    // Get the keysets for this mint
-    let keysets = wallet.get_mint_keysets(&mint_url).await?;
+    // Get the keysets for this mint using a wallet
+    let mint_wallet = wallet
+        .get_wallets_for_mint(&mint_url)
+        .await
+        .into_iter()
+        .next()
+        .ok_or("No wallet found for mint")?;
+    let keysets: Vec<cdk::nuts::KeySetInfo> = mint_wallet.get_mint_keysets().await?;
     println!("Found {} keysets for mint", keysets.len());
 
     for keyset in &keysets {

+ 8 - 0
crates/cdk/src/wallet/issue/mod.rs

@@ -257,6 +257,10 @@ impl Wallet {
         let mut updated_quotes = Vec::new();
 
         for mint_quote in mint_quotes {
+            if mint_quote.mint_url != self.mint_url || mint_quote.unit != self.unit {
+                continue;
+            }
+
             match self.inner_check_mint_quote_status(mint_quote).await {
                 Ok(q) => updated_quotes.push(q),
                 Err(err) => {
@@ -282,6 +286,10 @@ impl Wallet {
         let mut total_amount = Amount::ZERO;
 
         for mint_quote in mint_quotes {
+            if mint_quote.mint_url != self.mint_url || mint_quote.unit != self.unit {
+                continue;
+            }
+
             let current_amount_issued = mint_quote.amount_issued;
 
             let mint_quote = match self.inner_check_mint_quote_status(mint_quote).await {

+ 5 - 1
crates/cdk/src/wallet/melt/mod.rs

@@ -1058,11 +1058,15 @@ impl Wallet {
                     .client
                     .get_melt_quote_custom_status(method, quote_id)
                     .await?;
+                let change_amount = response
+                    .change
+                    .as_ref()
+                    .and_then(|change| Amount::try_sum(change.iter().map(|sig| sig.amount)).ok());
                 self.update_melt_quote_state(
                     &mut quote,
                     response.state,
                     response.amount,
-                    response.change_amount(),
+                    change_amount,
                     response.payment_preimage,
                 )
                 .await?;

+ 4 - 2
crates/cdk/src/wallet/mod.rs

@@ -47,7 +47,6 @@ mod keysets;
 mod melt;
 mod mint_connector;
 mod mint_metadata_cache;
-pub mod multi_mint_wallet;
 #[cfg(feature = "npubcash")]
 mod npubcash;
 pub mod payment_request;
@@ -64,6 +63,7 @@ mod swap;
 pub mod test_utils;
 mod transactions;
 pub mod util;
+pub mod wallet_repository;
 
 pub use auth::{AuthMintConnector, AuthWallet};
 pub use builder::WalletBuilder;
@@ -73,7 +73,6 @@ pub use mint_connector::transport::Transport as HttpTransport;
 pub use mint_connector::{
     AuthHttpClient, HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector,
 };
-pub use multi_mint_wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet};
 #[cfg(feature = "nostr")]
 pub use nostr_backup::{BackupOptions, BackupResult, RestoreOptions, RestoreResult};
 pub use payment_request::CreateRequestParams;
@@ -82,7 +81,10 @@ pub use payment_request::NostrWaitInfo;
 pub use receive::ReceiveOptions;
 pub use recovery::RecoveryReport;
 pub use send::{PreparedSend, SendMemo, SendOptions};
+#[cfg(all(feature = "npubcash", not(target_arch = "wasm32")))]
+pub use streams::npubcash::NpubCashProofStream;
 pub use types::{MeltQuote, MintQuote, SendKind};
+pub use wallet_repository::{TokenData, WalletConfig, WalletRepository, WalletRepositoryBuilder};
 
 use crate::nuts::nut00::ProofsMethods;
 

+ 0 - 3068
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -1,3068 +0,0 @@
-//! MultiMint Wallet
-//!
-//! Wrapper around core [`Wallet`] that enables the use of multiple mint unit
-//! pairs
-
-use std::collections::BTreeMap;
-use std::ops::Deref;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use anyhow::Result;
-use cdk_common::database::WalletDatabase;
-use cdk_common::task::spawn;
-use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection, TransactionId};
-use cdk_common::{database, KeySetInfo};
-use tokio::sync::RwLock;
-use tracing::instrument;
-use uuid::Uuid;
-use zeroize::Zeroize;
-
-use super::builder::WalletBuilder;
-use super::melt::MeltConfirmOptions;
-use super::receive::ReceiveOptions;
-use super::send::{SendMemo, SendOptions};
-use super::{Error, Restored};
-use crate::amount::SplitTarget;
-use crate::mint_url::MintUrl;
-use crate::nuts::nut00::ProofsMethods;
-use crate::nuts::nut23::QuoteState;
-use crate::nuts::{
-    CurrencyUnit, MeltOptions, PaymentMethod, Proof, Proofs, SpendingConditions, State, Token,
-};
-use crate::types::FinalizedMelt;
-#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-use crate::wallet::mint_connector::transport::tor_transport::TorAsync;
-use crate::wallet::types::MintQuote;
-use crate::{Amount, Wallet};
-
-// Transfer timeout constants
-/// Total timeout for waiting for Lightning payment confirmation during transfers
-/// This needs to be long enough to handle slow networks and Lightning routing
-const TRANSFER_PAYMENT_TIMEOUT_SECS: u64 = 120; // 2 minutes
-
-/// Transfer mode for mint-to-mint transfers
-#[derive(Debug, Clone)]
-pub enum TransferMode {
-    /// Transfer exact amount to target (target receives specified amount)
-    ExactReceive(Amount),
-    /// Transfer all available balance (source will be emptied)
-    FullBalance,
-}
-
-/// Result of a transfer operation with detailed breakdown
-#[derive(Debug, Clone)]
-pub struct TransferResult {
-    /// Amount deducted from source mint
-    pub amount_sent: Amount,
-    /// Amount received at target mint
-    pub amount_received: Amount,
-    /// Total fees paid for the transfer
-    pub fees_paid: Amount,
-    /// Remaining balance in source mint after transfer
-    pub source_balance_after: Amount,
-    /// New balance in target mint after transfer
-    pub target_balance_after: Amount,
-}
-
-/// Data extracted from a token including mint URL, proofs, and memo
-#[derive(Debug, Clone)]
-pub struct TokenData {
-    /// The mint URL from the token
-    pub mint_url: MintUrl,
-    /// The proofs contained in the token
-    pub proofs: Proofs,
-    /// The memo from the token, if present
-    pub memo: Option<String>,
-    /// Value of token
-    pub value: Amount,
-    /// Unit of token
-    pub unit: CurrencyUnit,
-    /// Fee to redeem
-    ///
-    /// If the token is for a proof that we do not know, we cannot get the fee.
-    /// To avoid just erroring and still allow decoding, this is an option.
-    /// None does not mean there is no fee, it means we do not know the fee.
-    pub redeem_fee: Option<Amount>,
-}
-
-/// Configuration for individual wallets within MultiMintWallet
-#[derive(Clone, Default, Debug)]
-pub struct WalletConfig {
-    /// Custom mint connector implementation
-    pub mint_connector: Option<Arc<dyn super::MintConnector + Send + Sync>>,
-    /// Custom auth connector implementation
-    pub auth_connector: Option<Arc<dyn super::auth::AuthMintConnector + Send + Sync>>,
-    /// Target number of proofs to maintain at each denomination
-    pub target_proof_count: Option<usize>,
-    /// Metadata cache TTL
-    ///
-    /// The TTL determines how often the wallet checks the mint for new keysets and information.
-    ///
-    /// If `None`, the cache will never expire and the wallet will use cached data indefinitely
-    /// (unless manually refreshed).
-    ///
-    /// The default value is 1 hour (3600 seconds).
-    pub metadata_cache_ttl: Option<std::time::Duration>,
-}
-
-impl WalletConfig {
-    /// Create a new empty WalletConfig
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    /// Set custom mint connector
-    pub fn with_mint_connector(
-        mut self,
-        connector: Arc<dyn super::MintConnector + Send + Sync>,
-    ) -> Self {
-        self.mint_connector = Some(connector);
-        self
-    }
-
-    /// Set custom auth connector
-    pub fn with_auth_connector(
-        mut self,
-        connector: Arc<dyn super::auth::AuthMintConnector + Send + Sync>,
-    ) -> Self {
-        self.auth_connector = Some(connector);
-        self
-    }
-
-    /// Set target proof count
-    pub fn with_target_proof_count(mut self, count: usize) -> Self {
-        self.target_proof_count = Some(count);
-        self
-    }
-
-    /// Set metadata cache TTL
-    ///
-    /// The TTL determines how often the wallet checks the mint for new keysets and information.
-    ///
-    /// If `None`, the cache will never expire and the wallet will use cached data indefinitely
-    /// (unless manually refreshed).
-    ///
-    /// The default value is 1 hour (3600 seconds).
-    pub fn with_metadata_cache_ttl(mut self, ttl: Option<std::time::Duration>) -> Self {
-        self.metadata_cache_ttl = ttl;
-        self
-    }
-}
-
-/// A prepared send operation from MultiMintWallet
-///
-/// This holds an `Arc<Wallet>` so it can call `.confirm()` without holding
-/// the RwLock. Created by [`MultiMintWallet::prepare_send`].
-#[must_use = "must be confirmed or canceled to release reserved proofs"]
-pub struct MultiMintPreparedSend {
-    wallet: Arc<Wallet>,
-    operation_id: Uuid,
-    amount: Amount,
-    options: SendOptions,
-    proofs_to_swap: Proofs,
-    proofs_to_send: Proofs,
-    swap_fee: Amount,
-    send_fee: Amount,
-}
-
-impl MultiMintPreparedSend {
-    /// Operation ID for this prepared send
-    pub fn operation_id(&self) -> Uuid {
-        self.operation_id
-    }
-
-    /// Amount to send
-    pub fn amount(&self) -> Amount {
-        self.amount
-    }
-
-    /// Send options
-    pub fn options(&self) -> &SendOptions {
-        &self.options
-    }
-
-    /// Proofs that need to be swapped before sending
-    pub fn proofs_to_swap(&self) -> &Proofs {
-        &self.proofs_to_swap
-    }
-
-    /// Fee for the swap operation
-    pub fn swap_fee(&self) -> Amount {
-        self.swap_fee
-    }
-
-    /// Proofs that will be sent directly
-    pub fn proofs_to_send(&self) -> &Proofs {
-        &self.proofs_to_send
-    }
-
-    /// Fee the recipient will pay to redeem the token
-    pub fn send_fee(&self) -> Amount {
-        self.send_fee
-    }
-
-    /// All proofs (both to swap and to send)
-    pub fn proofs(&self) -> Proofs {
-        let mut proofs = self.proofs_to_swap.clone();
-        proofs.extend(self.proofs_to_send.clone());
-        proofs
-    }
-
-    /// Total fee (swap + send)
-    pub fn fee(&self) -> Amount {
-        self.swap_fee + self.send_fee
-    }
-
-    /// Confirm the prepared send and create a token
-    pub async fn confirm(self, memo: Option<SendMemo>) -> Result<Token, Error> {
-        self.wallet
-            .confirm_send(
-                self.operation_id,
-                self.amount,
-                self.options,
-                self.proofs_to_swap,
-                self.proofs_to_send,
-                self.swap_fee,
-                self.send_fee,
-                memo,
-            )
-            .await
-    }
-
-    /// Cancel the prepared send and release reserved proofs
-    pub async fn cancel(self) -> Result<(), Error> {
-        self.wallet
-            .cancel_send(self.operation_id, self.proofs_to_swap, self.proofs_to_send)
-            .await
-    }
-}
-
-impl std::fmt::Debug for MultiMintPreparedSend {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("MultiMintPreparedSend")
-            .field("operation_id", &self.operation_id)
-            .field("amount", &self.amount)
-            .field("swap_fee", &self.swap_fee)
-            .field("send_fee", &self.send_fee)
-            .finish()
-    }
-}
-
-/// A prepared melt operation from MultiMintWallet
-///
-/// This holds an `Arc<Wallet>` so it can call `.confirm()` without holding
-/// the RwLock. Created by [`MultiMintWallet::prepare_melt`].
-#[must_use = "must be confirmed or canceled to release reserved proofs"]
-pub struct MultiMintPreparedMelt {
-    wallet: Arc<Wallet>,
-    operation_id: Uuid,
-    quote: MeltQuote,
-    proofs: Proofs,
-    proofs_to_swap: Proofs,
-    swap_fee: Amount,
-    input_fee: Amount,
-    input_fee_without_swap: Amount,
-    metadata: std::collections::HashMap<String, String>,
-}
-
-impl MultiMintPreparedMelt {
-    /// Get the operation ID
-    pub fn operation_id(&self) -> Uuid {
-        self.operation_id
-    }
-
-    /// Get the quote
-    pub fn quote(&self) -> &MeltQuote {
-        &self.quote
-    }
-
-    /// Get the amount to be melted
-    pub fn amount(&self) -> Amount {
-        self.quote.amount
-    }
-
-    /// Get the proofs that will be used
-    pub fn proofs(&self) -> &Proofs {
-        &self.proofs
-    }
-
-    /// Get the proofs that need to be swapped
-    pub fn proofs_to_swap(&self) -> &Proofs {
-        &self.proofs_to_swap
-    }
-
-    /// Get the swap fee
-    pub fn swap_fee(&self) -> Amount {
-        self.swap_fee
-    }
-
-    /// Get the input fee
-    pub fn input_fee(&self) -> Amount {
-        self.input_fee
-    }
-
-    /// Get the total fee (with swap, if applicable)
-    pub fn total_fee(&self) -> Amount {
-        self.swap_fee + self.input_fee
-    }
-
-    /// Returns true if a swap would be performed (proofs_to_swap is not empty)
-    pub fn requires_swap(&self) -> bool {
-        !self.proofs_to_swap.is_empty()
-    }
-
-    /// Get the total fee if swap is performed (current default behavior)
-    ///
-    /// This is swap_fee + input_fee on optimized proofs.
-    /// Same as [`total_fee()`](Self::total_fee).
-    pub fn total_fee_with_swap(&self) -> Amount {
-        self.swap_fee + self.input_fee
-    }
-
-    /// Get the input fee if swap is skipped (fee on all proofs sent directly)
-    pub fn input_fee_without_swap(&self) -> Amount {
-        self.input_fee_without_swap
-    }
-
-    /// Get the fee savings from skipping the swap
-    ///
-    /// Returns how much less you would pay in fees by using
-    /// `confirm_with_options(MeltConfirmOptions::skip_swap())`.
-    pub fn fee_savings_without_swap(&self) -> Amount {
-        self.total_fee_with_swap()
-            .checked_sub(self.input_fee_without_swap)
-            .unwrap_or(Amount::ZERO)
-    }
-
-    /// Get the expected change amount if swap is skipped
-    ///
-    /// This is how much would be "overpaid" and returned as change from the melt.
-    pub fn change_amount_without_swap(&self) -> Amount {
-        let all_proofs_total = self.proofs.total_amount().unwrap_or(Amount::ZERO)
-            + self.proofs_to_swap.total_amount().unwrap_or(Amount::ZERO);
-        let needed = self.quote.amount + self.quote.fee_reserve + self.input_fee_without_swap;
-        all_proofs_total.checked_sub(needed).unwrap_or(Amount::ZERO)
-    }
-
-    /// Confirm the prepared melt and execute the payment
-    pub async fn confirm(self) -> Result<FinalizedMelt, Error> {
-        self.confirm_with_options(MeltConfirmOptions::default())
-            .await
-    }
-
-    /// Confirm the prepared melt with custom options
-    ///
-    /// # Options
-    ///
-    /// - `skip_swap`: If true, skips the pre-melt swap and sends proofs directly.
-    pub async fn confirm_with_options(
-        self,
-        options: MeltConfirmOptions,
-    ) -> Result<FinalizedMelt, Error> {
-        self.wallet
-            .confirm_prepared_melt_with_options(
-                self.operation_id,
-                self.quote,
-                self.proofs,
-                self.proofs_to_swap,
-                self.input_fee,
-                self.input_fee_without_swap,
-                self.metadata,
-                options,
-            )
-            .await
-    }
-
-    /// Cancel the prepared melt and release reserved proofs
-    pub async fn cancel(self) -> Result<(), Error> {
-        self.wallet
-            .cancel_prepared_melt(self.operation_id, self.proofs, self.proofs_to_swap)
-            .await
-    }
-}
-
-impl std::fmt::Debug for MultiMintPreparedMelt {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("MultiMintPreparedMelt")
-            .field("operation_id", &self.operation_id)
-            .field("quote_id", &self.quote.id)
-            .field("amount", &self.quote.amount)
-            .field("total_fee", &self.total_fee())
-            .finish()
-    }
-}
-
-/// Multi Mint Wallet
-///
-/// A wallet that manages multiple mints but supports only one currency unit.
-/// This simplifies the interface by removing the need to specify both mint and unit.
-///
-/// # Examples
-///
-/// ## Creating and using a multi-mint wallet
-/// ```ignore
-/// # use cdk::wallet::MultiMintWallet;
-/// # use cdk::mint_url::MintUrl;
-/// # use cdk::Amount;
-/// # use cdk::nuts::CurrencyUnit;
-/// # use std::sync::Arc;
-/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
-/// // Create a multi-mint wallet with a database
-/// // For real usage, you would use cdk_sqlite::wallet::memory::empty().await? or similar
-/// let seed = [0u8; 64];  // Use a secure random seed in production
-/// let database = cdk_sqlite::wallet::memory::empty().await?;
-///
-/// let wallet = MultiMintWallet::new(
-///     Arc::new(database),
-///     seed,
-///     CurrencyUnit::Sat,
-/// ).await?;
-///
-/// // Add mints to the wallet
-/// let mint_url1: MintUrl = "https://mint1.example.com".parse()?;
-/// let mint_url2: MintUrl = "https://mint2.example.com".parse()?;
-/// wallet.add_mint(mint_url1.clone()).await?;
-/// wallet.add_mint(mint_url2).await?;
-///
-/// // Check total balance across all mints
-/// let balance = wallet.total_balance().await?;
-/// println!("Total balance: {} sats", balance);
-///
-/// // Send tokens from a specific mint
-/// let token = wallet.send(
-///     mint_url1,
-///     Amount::from(100),
-///     Default::default()
-/// ).await?;
-/// # Ok(())
-/// # }
-/// ```
-#[derive(Clone)]
-pub struct MultiMintWallet {
-    /// Storage backend
-    localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
-    seed: [u8; 64],
-    /// The currency unit this wallet supports
-    unit: CurrencyUnit,
-    /// Wallets indexed by mint URL (wrapped in Arc for sharing)
-    wallets: Arc<RwLock<BTreeMap<MintUrl, Arc<Wallet>>>>,
-    /// Proxy configuration for HTTP clients (optional)
-    proxy_config: Option<url::Url>,
-    /// Shared Tor transport to be cloned into each TorHttpClient (if enabled)
-    #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-    shared_tor_transport: Option<TorAsync>,
-}
-
-impl std::fmt::Debug for MultiMintWallet {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("MultiMintWallet")
-            .field("unit", &self.unit)
-            .finish_non_exhaustive()
-    }
-}
-
-impl MultiMintWallet {
-    /// Create a new [MultiMintWallet] for a specific currency unit
-    pub async fn new(
-        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
-        seed: [u8; 64],
-        unit: CurrencyUnit,
-    ) -> Result<Self, Error> {
-        let wallet = Self {
-            localstore,
-            seed,
-            unit,
-            wallets: Arc::new(RwLock::new(BTreeMap::new())),
-            proxy_config: None,
-            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-            shared_tor_transport: None,
-        };
-
-        // Automatically load wallets from database for this currency unit
-        wallet.load_wallets().await?;
-
-        Ok(wallet)
-    }
-
-    /// Create a new [MultiMintWallet] with proxy configuration
-    ///
-    /// All wallets in this MultiMintWallet will use the specified proxy.
-    /// This allows you to route all mint connections through a proxy server.
-    pub async fn new_with_proxy(
-        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
-        seed: [u8; 64],
-        unit: CurrencyUnit,
-        proxy_url: url::Url,
-    ) -> Result<Self, Error> {
-        let wallet = Self {
-            localstore,
-            seed,
-            unit,
-            wallets: Arc::new(RwLock::new(BTreeMap::new())),
-            proxy_config: Some(proxy_url),
-            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-            shared_tor_transport: None,
-        };
-
-        // Automatically load wallets from database for this currency unit
-        wallet.load_wallets().await?;
-
-        Ok(wallet)
-    }
-
-    /// Create a new [MultiMintWallet] with Tor transport for all wallets
-    ///
-    /// When the `tor` feature is enabled (and not on wasm32), this constructor
-    /// creates a single Tor transport (TorAsync) that is cloned into each
-    /// TorHttpClient used by per-mint Wallets. This ensures only one Tor instance
-    /// is bootstrapped and shared across wallets.
-    #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-    pub async fn new_with_tor(
-        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
-        seed: [u8; 64],
-        unit: CurrencyUnit,
-    ) -> Result<Self, Error> {
-        let wallet = Self {
-            localstore,
-            seed,
-            unit,
-            wallets: Arc::new(RwLock::new(BTreeMap::new())),
-            proxy_config: None,
-            shared_tor_transport: Some(TorAsync::new()),
-        };
-
-        // Automatically load wallets from database for this currency unit
-        wallet.load_wallets().await?;
-
-        Ok(wallet)
-    }
-
-    /// Get a reference to the wallet seed
-    ///
-    /// This is used internally for key derivation operations.
-    #[inline(always)]
-    #[cfg(all(feature = "wallet", feature = "nostr"))]
-    pub(crate) fn seed(&self) -> &[u8; 64] {
-        &self.seed
-    }
-
-    /// Adds a mint to this [MultiMintWallet]
-    ///
-    /// Creates a wallet for the specified mint using default or global settings.
-    /// For custom configuration, use `add_mint_with_config()`.
-    #[instrument(skip(self))]
-    pub async fn add_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
-        // Create wallet with default settings
-        let wallet = self
-            .create_wallet_with_config(mint_url.clone(), None)
-            .await?;
-
-        // Insert into wallets map (wrapped in Arc)
-        let mut wallets = self.wallets.write().await;
-        wallets.insert(mint_url, Arc::new(wallet));
-
-        Ok(())
-    }
-
-    /// Adds a mint to this [MultiMintWallet] with custom configuration
-    ///
-    /// The provided configuration is used to create the wallet with custom connectors
-    /// and settings. Configuration is stored within the Wallet instance itself.
-    #[instrument(skip(self))]
-    pub async fn add_mint_with_config(
-        &self,
-        mint_url: MintUrl,
-        config: WalletConfig,
-    ) -> Result<(), Error> {
-        // Create wallet with the provided config
-        let wallet = self
-            .create_wallet_with_config(mint_url.clone(), Some(&config))
-            .await?;
-
-        // Insert into wallets map (wrapped in Arc)
-        let mut wallets = self.wallets.write().await;
-        wallets.insert(mint_url, Arc::new(wallet));
-
-        Ok(())
-    }
-
-    /// Set or update configuration for a mint
-    ///
-    /// If the wallet already exists, it will be updated with the new config.
-    /// If the wallet doesn't exist, it will be created with the specified config.
-    #[instrument(skip(self))]
-    pub async fn set_mint_config(
-        &self,
-        mint_url: MintUrl,
-        config: WalletConfig,
-    ) -> Result<(), Error> {
-        // Check if wallet already exists
-        if self.has_mint(&mint_url).await {
-            // Update existing wallet in place
-            let mut wallets = self.wallets.write().await;
-            if let Some(wallet_arc) = wallets.get_mut(&mint_url) {
-                // Try to get mutable access - fails if there are other Arc references
-                let wallet = Arc::get_mut(wallet_arc).ok_or_else(|| {
-                    Error::Custom(
-                        "Cannot modify wallet config while operations are in progress".to_string(),
-                    )
-                })?;
-
-                // Update target_proof_count if provided
-                if let Some(count) = config.target_proof_count {
-                    wallet.set_target_proof_count(count);
-                }
-
-                // Update connector if provided
-                if let Some(connector) = config.mint_connector {
-                    wallet.set_client(connector);
-                }
-
-                // Update metadata cache TTL if provided
-                if let Some(ttl) = config.metadata_cache_ttl {
-                    wallet.set_metadata_cache_ttl(Some(ttl));
-                }
-
-                // TODO: Handle auth_connector if provided
-                if let Some(_auth_connector) = config.auth_connector {
-                    // For now, we can't easily inject auth_connector into the wallet
-                    // This would require additional work on the Wallet API
-                    // We'll note this as a future enhancement
-                }
-            }
-            Ok(())
-        } else {
-            // Wallet doesn't exist, create it with the provided config
-            self.add_mint_with_config(mint_url, config).await
-        }
-    }
-
-    /// Set the auth client (AuthWallet) for a specific mint
-    ///
-    /// This allows updating the auth wallet for an existing mint wallet without recreating it.
-    #[instrument(skip_all)]
-    pub async fn set_auth_client(
-        &self,
-        mint_url: &MintUrl,
-        auth_wallet: Option<super::auth::AuthWallet>,
-    ) -> Result<(), Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.set_auth_client(auth_wallet).await;
-        Ok(())
-    }
-
-    /// Remove mint from MultiMintWallet
-    #[instrument(skip(self))]
-    pub async fn remove_mint(&self, mint_url: &MintUrl) {
-        let mut wallets = self.wallets.write().await;
-        wallets.remove(mint_url);
-    }
-
-    /// Update the mint URL for an existing wallet
-    ///
-    /// This updates the mint URL in the database and recreates the wallet with the new URL.
-    /// Returns an error if the old mint URL doesn't exist or if there are active operations
-    /// on the wallet.
-    #[instrument(skip(self))]
-    pub async fn update_mint_url(
-        &self,
-        old_mint_url: &MintUrl,
-        new_mint_url: MintUrl,
-    ) -> Result<(), Error> {
-        // Get write lock and check if wallet exists
-        let mut wallets = self.wallets.write().await;
-
-        // Remove old wallet - this will fail if there are other Arc references
-        let old_wallet_arc = wallets.remove(old_mint_url).ok_or(Error::UnknownMint {
-            mint_url: old_mint_url.to_string(),
-        })?;
-
-        // Check that we're the only holder of this Arc
-        // If not, someone else is using the wallet (e.g., PreparedSend)
-        let old_wallet = Arc::try_unwrap(old_wallet_arc).map_err(|_| {
-            Error::Custom("Cannot update mint URL while operations are in progress".to_string())
-        })?;
-
-        // Update the database
-        self.localstore
-            .update_mint_url(old_mint_url.clone(), new_mint_url.clone())
-            .await
-            .map_err(Error::Database)?;
-
-        // Create a new wallet with the new URL
-        // We drop the old wallet and create fresh to ensure clean state
-        drop(old_wallet);
-        let new_wallet = self
-            .create_wallet_with_config(new_mint_url.clone(), None)
-            .await?;
-
-        // Insert the new wallet
-        wallets.insert(new_mint_url, Arc::new(new_wallet));
-
-        Ok(())
-    }
-
-    /// Internal: Create wallet with optional custom configuration
-    ///
-    /// Priority order for configuration:
-    /// 1. Custom connector from config (if provided)
-    /// 2. Global settings (proxy/Tor)
-    /// 3. Default HttpClient
-    async fn create_wallet_with_config(
-        &self,
-        mint_url: MintUrl,
-        config: Option<&WalletConfig>,
-    ) -> Result<Wallet, Error> {
-        // Check if custom connector is provided in config
-        if let Some(cfg) = config {
-            if let Some(custom_connector) = &cfg.mint_connector {
-                // Use custom connector with WalletBuilder
-                let mut builder = WalletBuilder::new()
-                    .mint_url(mint_url.clone())
-                    .unit(self.unit.clone())
-                    .localstore(self.localstore.clone())
-                    .seed(self.seed)
-                    .target_proof_count(cfg.target_proof_count.unwrap_or(3))
-                    .shared_client(custom_connector.clone());
-
-                if let Some(ttl) = cfg.metadata_cache_ttl {
-                    builder = builder.set_metadata_cache_ttl(Some(ttl));
-                }
-
-                // TODO: Handle auth_connector if provided
-                if let Some(_auth_connector) = &cfg.auth_connector {
-                    // For now, we can't easily inject auth_connector into the wallet
-                    // This would require additional work on the Wallet/WalletBuilder API
-                    // We'll note this as a future enhancement
-                }
-
-                return builder.build();
-            }
-        }
-
-        // Fall back to existing logic: proxy/Tor/default
-        let target_proof_count = config.and_then(|c| c.target_proof_count).unwrap_or(3);
-        let metadata_cache_ttl = config.and_then(|c| c.metadata_cache_ttl);
-
-        let wallet = if let Some(proxy_url) = &self.proxy_config {
-            // Create wallet with proxy-configured client
-            let client = crate::wallet::HttpClient::with_proxy(
-                mint_url.clone(),
-                proxy_url.clone(),
-                None,
-                true,
-            )
-            .unwrap_or_else(|_| crate::wallet::HttpClient::new(mint_url.clone(), None));
-            let mut builder = WalletBuilder::new()
-                .mint_url(mint_url.clone())
-                .unit(self.unit.clone())
-                .localstore(self.localstore.clone())
-                .seed(self.seed)
-                .target_proof_count(target_proof_count)
-                .client(client);
-
-            if let Some(ttl) = metadata_cache_ttl {
-                builder = builder.set_metadata_cache_ttl(Some(ttl));
-            }
-
-            builder.build()?
-        } else {
-            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
-            if let Some(tor) = &self.shared_tor_transport {
-                // Create wallet with Tor transport client, cloning the shared transport
-                let client = {
-                    let transport = tor.clone();
-                    crate::wallet::TorHttpClient::with_transport(mint_url.clone(), transport, None)
-                };
-
-                let mut builder = WalletBuilder::new()
-                    .mint_url(mint_url.clone())
-                    .unit(self.unit.clone())
-                    .localstore(self.localstore.clone())
-                    .seed(self.seed)
-                    .target_proof_count(target_proof_count)
-                    .client(client);
-
-                if let Some(ttl) = metadata_cache_ttl {
-                    builder = builder.set_metadata_cache_ttl(Some(ttl));
-                }
-
-                builder.build()?
-            } else {
-                // Create wallet with default client
-                let wallet = Wallet::new(
-                    &mint_url.to_string(),
-                    self.unit.clone(),
-                    self.localstore.clone(),
-                    self.seed,
-                    Some(target_proof_count),
-                )?;
-                if let Some(ttl) = metadata_cache_ttl {
-                    wallet.set_metadata_cache_ttl(Some(ttl));
-                }
-                wallet
-            }
-
-            #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
-            {
-                // Create wallet with default client
-                let wallet = Wallet::new(
-                    &mint_url.to_string(),
-                    self.unit.clone(),
-                    self.localstore.clone(),
-                    self.seed,
-                    Some(target_proof_count),
-                )?;
-                if let Some(ttl) = metadata_cache_ttl {
-                    wallet.set_metadata_cache_ttl(Some(ttl));
-                }
-                wallet
-            }
-        };
-
-        Ok(wallet)
-    }
-
-    /// Load all wallets from database that have proofs for this currency unit
-    #[instrument(skip(self))]
-    async fn load_wallets(&self) -> Result<(), Error> {
-        let mints = self.localstore.get_mints().await.map_err(Error::Database)?;
-
-        // Get all proofs for this currency unit to determine which mints are relevant
-        let all_proofs = self
-            .localstore
-            .get_proofs(None, Some(self.unit.clone()), None, None)
-            .await
-            .map_err(Error::Database)?;
-
-        for (mint_url, _mint_info) in mints {
-            // Check if this mint has any proofs for the specified currency unit
-            // or if we have no proofs at all (initial setup)
-            let mint_has_proofs_for_unit =
-                all_proofs.is_empty() || all_proofs.iter().any(|proof| proof.mint_url == mint_url);
-
-            if mint_has_proofs_for_unit {
-                // Add mint to the MultiMintWallet if not already present
-                if !self.has_mint(&mint_url).await {
-                    self.add_mint(mint_url.clone()).await?
-                }
-            }
-        }
-
-        Ok(())
-    }
-
-    /// Get Wallets from MultiMintWallet
-    #[instrument(skip(self))]
-    pub async fn get_wallets(&self) -> Vec<Arc<Wallet>> {
-        self.wallets.read().await.values().cloned().collect()
-    }
-
-    /// Get Wallet from MultiMintWallet
-    #[instrument(skip(self))]
-    pub async fn get_wallet(&self, mint_url: &MintUrl) -> Option<Arc<Wallet>> {
-        self.wallets.read().await.get(mint_url).cloned()
-    }
-
-    /// Check if mint is in wallet
-    #[instrument(skip(self))]
-    pub async fn has_mint(&self, mint_url: &MintUrl) -> bool {
-        self.wallets.read().await.contains_key(mint_url)
-    }
-
-    /// Get the currency unit for this wallet
-    pub fn unit(&self) -> &CurrencyUnit {
-        &self.unit
-    }
-
-    /// Get keysets for a mint url
-    pub async fn get_mint_keysets(&self, mint_url: &MintUrl) -> Result<Vec<KeySetInfo>, Error> {
-        let wallets = self.wallets.read().await;
-        let target_wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        target_wallet.get_mint_keysets().await
-    }
-
-    /// Get token data (mint URL and proofs) from a token
-    ///
-    /// This method extracts the mint URL and proofs from a token. It will automatically
-    /// fetch the keysets from the mint if needed to properly decode the proofs.
-    ///
-    /// The mint must already be added to the wallet. If the mint is not in the wallet,
-    /// use `add_mint` first or set `allow_untrusted` in receive options.
-    ///
-    /// # Arguments
-    ///
-    /// * `token` - The token to extract data from
-    ///
-    /// # Returns
-    ///
-    /// A `TokenData` struct containing the mint URL and proofs
-    ///
-    /// # Example
-    ///
-    /// ```no_run
-    /// # use cdk::wallet::MultiMintWallet;
-    /// # use cdk::nuts::Token;
-    /// # use std::str::FromStr;
-    /// # async fn example(wallet: &MultiMintWallet) -> Result<(), Box<dyn std::error::Error>> {
-    /// let token = Token::from_str("cashuA...")?;
-    /// let token_data = wallet.get_token_data(&token).await?;
-    /// println!("Mint: {}", token_data.mint_url);
-    /// println!("Proofs: {} total", token_data.proofs.len());
-    /// # Ok(())
-    /// # }
-    /// ```
-    #[instrument(skip(self, token))]
-    pub async fn get_token_data(&self, token: &Token) -> Result<TokenData, Error> {
-        let mint_url = token.mint_url()?;
-
-        // Get the keysets for this mint
-        let keysets = self.get_mint_keysets(&mint_url).await?;
-
-        // Extract proofs using the keysets
-        let proofs = token.proofs(&keysets)?;
-
-        // Get the memo
-        let memo = token.memo().clone();
-
-        let redeem_fee = self.get_proofs_fee(&mint_url, &proofs).await.ok();
-
-        Ok(TokenData {
-            value: proofs.total_amount()?,
-            mint_url,
-            proofs,
-            memo,
-            unit: token.unit().unwrap_or_default(),
-            redeem_fee,
-        })
-    }
-
-    /// Get wallet balances for all mints
-    #[instrument(skip(self))]
-    pub async fn get_balances(&self) -> Result<BTreeMap<MintUrl, Amount>, Error> {
-        let mut balances = BTreeMap::new();
-
-        for (mint_url, wallet) in self.wallets.read().await.iter() {
-            let wallet_balance = wallet.total_balance().await?;
-            balances.insert(mint_url.clone(), wallet_balance);
-        }
-
-        Ok(balances)
-    }
-
-    /// List proofs.
-    #[instrument(skip(self))]
-    pub async fn list_proofs(&self) -> Result<BTreeMap<MintUrl, Vec<Proof>>, Error> {
-        let mut mint_proofs = BTreeMap::new();
-
-        for (mint_url, wallet) in self.wallets.read().await.iter() {
-            let wallet_proofs = wallet.get_unspent_proofs().await?;
-            mint_proofs.insert(mint_url.clone(), wallet_proofs);
-        }
-        Ok(mint_proofs)
-    }
-
-    /// NUT-07 Check the state of proofs with a specific mint
-    #[instrument(skip(self, proofs))]
-    pub async fn check_proofs_state(
-        &self,
-        mint_url: &MintUrl,
-        proofs: Proofs,
-    ) -> Result<Vec<State>, Error> {
-        let wallet = self.get_wallet(mint_url).await.ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-        let states = wallet.check_proofs_spent(proofs).await?;
-        Ok(states.into_iter().map(|s| s.state).collect())
-    }
-
-    /// Fee required to redeem proof set
-    #[instrument(skip(self, proofs))]
-    pub async fn get_proofs_fee(
-        &self,
-        mint_url: &MintUrl,
-        proofs: &Proofs,
-    ) -> Result<Amount, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        Ok(wallet.get_proofs_fee(proofs).await?.total)
-    }
-
-    /// List transactions
-    #[instrument(skip(self))]
-    pub async fn list_transactions(
-        &self,
-        direction: Option<TransactionDirection>,
-    ) -> Result<Vec<Transaction>, Error> {
-        let mut transactions = Vec::new();
-
-        for (_, wallet) in self.wallets.read().await.iter() {
-            let wallet_transactions = wallet.list_transactions(direction).await?;
-            transactions.extend(wallet_transactions);
-        }
-
-        transactions.sort();
-
-        Ok(transactions)
-    }
-
-    /// Get proofs for a transaction by transaction ID
-    ///
-    /// This retrieves all proofs associated with a transaction. If `mint_url` is provided,
-    /// it will only check that specific mint's wallet. Otherwise, it searches across all
-    /// wallets to find which mint the transaction belongs to.
-    ///
-    /// # Arguments
-    ///
-    /// * `id` - The transaction ID
-    /// * `mint_url` - Optional mint URL to check directly, avoiding iteration over all wallets
-    #[instrument(skip(self))]
-    pub async fn get_proofs_for_transaction(
-        &self,
-        id: TransactionId,
-        mint_url: Option<MintUrl>,
-    ) -> Result<Proofs, Error> {
-        let wallets = self.wallets.read().await;
-
-        // If mint_url is provided, try that wallet directly
-        if let Some(mint_url) = mint_url {
-            if let Some(wallet) = wallets.get(&mint_url) {
-                // Verify the transaction exists in this wallet
-                if wallet.get_transaction(id).await?.is_some() {
-                    return wallet.get_proofs_for_transaction(id).await;
-                }
-            }
-            // Transaction not found in specified mint
-            return Err(Error::TransactionNotFound);
-        }
-
-        // No mint_url provided, search across all wallets
-        for (mint_url, wallet) in wallets.iter() {
-            if let Some(transaction) = wallet.get_transaction(id).await? {
-                // Verify the transaction belongs to this wallet's mint
-                if &transaction.mint_url == mint_url {
-                    return wallet.get_proofs_for_transaction(id).await;
-                }
-            }
-        }
-
-        // Transaction not found in any wallet
-        Err(Error::TransactionNotFound)
-    }
-
-    /// Get total balance across all wallets (since all wallets use the same currency unit)
-    #[instrument(skip(self))]
-    pub async fn total_balance(&self) -> Result<Amount, Error> {
-        let mut total = Amount::ZERO;
-        for (_, wallet) in self.wallets.read().await.iter() {
-            total += wallet.total_balance().await?;
-        }
-        Ok(total)
-    }
-
-    /// Prepare a send operation from a specific mint
-    ///
-    /// Returns a [`MultiMintPreparedSend`] that holds an `Arc<Wallet>` and can be
-    /// confirmed later by calling `.confirm()`. This does not support automatic
-    /// transfers from other mints - use [`send`](Self::send) for that.
-    ///
-    /// # Example
-    /// ```ignore
-    /// let prepared = wallet.prepare_send(mint_url, amount, options).await?;
-    /// // Inspect the prepared send...
-    /// println!("Fee: {}", prepared.fee());
-    /// // Then confirm or cancel
-    /// let token = prepared.confirm(None).await?;
-    /// ```
-    #[instrument(skip(self))]
-    pub async fn prepare_send(
-        &self,
-        mint_url: MintUrl,
-        amount: Amount,
-        opts: SendOptions,
-    ) -> Result<MultiMintPreparedSend, Error> {
-        // Clone the Arc<Wallet> and release the lock immediately
-        let wallet = {
-            let wallets = self.wallets.read().await;
-            wallets
-                .get(&mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone()
-        };
-
-        // Call prepare_send on the wallet (lock is released)
-        let prepared = wallet.prepare_send(amount, opts.clone()).await?;
-
-        // Extract data into MultiMintPreparedSend
-        // Clone the Arc again since `prepared` borrows from `wallet`
-        Ok(MultiMintPreparedSend {
-            wallet: Arc::clone(&wallet),
-            operation_id: prepared.operation_id(),
-            amount: prepared.amount(),
-            options: opts,
-            proofs_to_swap: prepared.proofs_to_swap().clone(),
-            proofs_to_send: prepared.proofs_to_send().clone(),
-            swap_fee: prepared.swap_fee(),
-            send_fee: prepared.send_fee(),
-        })
-    }
-
-    /// Send tokens from a specific mint with optional transfer from other mints
-    ///
-    /// This method ensures that sends always happen from only one mint. If the specified
-    /// mint doesn't have sufficient balance and `allow_transfer` is enabled in options,
-    /// it will first transfer funds from other mints to the target mint.
-    #[instrument(skip(self))]
-    pub async fn send(
-        &self,
-        mint_url: MintUrl,
-        amount: Amount,
-        opts: MultiMintSendOptions,
-    ) -> Result<Token, Error> {
-        // Ensure the mint exists
-        let wallets = self.wallets.read().await;
-        let target_wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        // Check current balance of target mint
-        let target_balance = target_wallet.total_balance().await?;
-
-        // If target mint has sufficient balance, send directly
-        if target_balance >= amount {
-            let prepared = target_wallet
-                .prepare_send(amount, opts.send_options.clone())
-                .await?;
-            return prepared.confirm(opts.send_options.memo).await;
-        }
-
-        // If transfer is not allowed, return insufficient funds error
-        if !opts.allow_transfer {
-            return Err(Error::InsufficientFunds);
-        }
-
-        // Calculate how much we need to transfer
-        let transfer_needed = amount - target_balance;
-
-        // Check if transfer amount exceeds max_transfer_amount
-        if let Some(max_transfer) = opts.max_transfer_amount {
-            if transfer_needed > max_transfer {
-                return Err(Error::InsufficientFunds);
-            }
-        }
-
-        // Find source wallets with available funds for transfer
-        let mut available_for_transfer = Amount::ZERO;
-        let mut source_mints = Vec::new();
-
-        for (source_mint_url, wallet) in wallets.iter() {
-            if source_mint_url == &mint_url {
-                continue; // Skip the target mint
-            }
-
-            // Check if this mint is excluded from transfers
-            if opts.excluded_mints.contains(source_mint_url) {
-                continue;
-            }
-
-            // Check if we have a restricted allowed list and this mint isn't in it
-            if !opts.allowed_mints.is_empty() && !opts.allowed_mints.contains(source_mint_url) {
-                continue;
-            }
-
-            let balance = wallet.total_balance().await?;
-            if balance > Amount::ZERO {
-                source_mints.push((source_mint_url.clone(), balance));
-                available_for_transfer += balance;
-            }
-        }
-
-        // Check if we have enough funds across all mints
-        if available_for_transfer < transfer_needed {
-            return Err(Error::InsufficientFunds);
-        }
-
-        // Drop the read lock before performing transfers
-        drop(wallets);
-
-        // Perform transfers from source wallets to target wallet
-        self.transfer_parallel(&mint_url, transfer_needed, source_mints)
-            .await?;
-
-        // Now send from the target mint
-        let wallets = self.wallets.read().await;
-        let target_wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        let prepared = target_wallet
-            .prepare_send(amount, opts.send_options.clone())
-            .await?;
-        prepared.confirm(opts.send_options.memo).await
-    }
-
-    /// Transfer funds from a single source wallet to target mint using Lightning Network (melt/mint)
-    ///
-    /// This function properly accounts for fees by handling different transfer modes:
-    /// - ExactReceive: Target receives exactly the specified amount, source pays amount + fees
-    /// - FullBalance: All source balance is transferred, target receives balance - fees
-    pub async fn transfer(
-        &self,
-        source_mint_url: &MintUrl,
-        target_mint_url: &MintUrl,
-        mode: TransferMode,
-    ) -> Result<TransferResult, Error> {
-        // Get wallets for the specified mints and clone them to release the lock
-        let (source_wallet, target_wallet) = {
-            let wallets = self.wallets.read().await;
-            let source = wallets
-                .get(source_mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: source_mint_url.to_string(),
-                })?
-                .clone();
-            let target = wallets
-                .get(target_mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: target_mint_url.to_string(),
-                })?
-                .clone();
-            (source, target)
-        };
-
-        // Get initial balance
-        let source_balance_initial = source_wallet.total_balance().await?;
-
-        // Handle different transfer modes
-        let (final_mint_quote, final_melt_quote) = match mode {
-            TransferMode::ExactReceive(amount) => {
-                self.handle_exact_receive_transfer(
-                    &source_wallet,
-                    &target_wallet,
-                    amount,
-                    source_balance_initial,
-                )
-                .await?
-            }
-            TransferMode::FullBalance => {
-                self.handle_full_balance_transfer(
-                    &source_wallet,
-                    &target_wallet,
-                    source_balance_initial,
-                )
-                .await?
-            }
-        };
-
-        // Execute the transfer
-        let (melted, actual_receive_amount) = self
-            .execute_transfer(
-                &source_wallet,
-                &target_wallet,
-                &final_mint_quote,
-                &final_melt_quote,
-            )
-            .await?;
-
-        // Get final balances
-        let source_balance_final = source_wallet.total_balance().await?;
-        let target_balance_final = target_wallet.total_balance().await?;
-
-        let amount_sent = source_balance_initial - source_balance_final;
-        let fees_paid = melted.fee_paid();
-
-        tracing::info!(
-            "Transferred {} from {} to {} via Lightning (sent: {} sats, received: {} sats, fee: {} sats)",
-            amount_sent,
-            source_wallet.mint_url,
-            target_wallet.mint_url,
-            amount_sent,
-            actual_receive_amount,
-            fees_paid
-        );
-
-        Ok(TransferResult {
-            amount_sent,
-            amount_received: actual_receive_amount,
-            fees_paid,
-            source_balance_after: source_balance_final,
-            target_balance_after: target_balance_final,
-        })
-    }
-
-    /// Handle exact receive transfer mode - target gets exactly the specified amount
-    async fn handle_exact_receive_transfer(
-        &self,
-        source_wallet: &Wallet,
-        target_wallet: &Wallet,
-        amount: Amount,
-        source_balance: Amount,
-    ) -> Result<(MintQuote, crate::wallet::types::MeltQuote), Error> {
-        // Step 1: Create mint quote at target mint for the exact amount we want to receive
-        let mint_quote = target_wallet
-            .mint_quote(PaymentMethod::BOLT11, Some(amount), None, None)
-            .await?;
-
-        // Step 2: Create melt quote at source mint for the invoice
-        let melt_quote = source_wallet
-            .melt_quote(
-                PaymentMethod::BOLT11,
-                mint_quote.request.clone(),
-                None,
-                None,
-            )
-            .await?;
-
-        // Step 3: Check if source has enough balance for the total amount needed (amount + melt fees)
-        let total_needed = melt_quote.amount + melt_quote.fee_reserve;
-        if source_balance < total_needed {
-            return Err(Error::InsufficientFunds);
-        }
-
-        Ok((mint_quote, melt_quote))
-    }
-
-    /// Handle full balance transfer mode - all source balance is transferred
-    async fn handle_full_balance_transfer(
-        &self,
-        source_wallet: &Wallet,
-        target_wallet: &Wallet,
-        source_balance: Amount,
-    ) -> Result<(MintQuote, crate::wallet::types::MeltQuote), Error> {
-        if source_balance == Amount::ZERO {
-            return Err(Error::InsufficientFunds);
-        }
-
-        // Step 1: Create melt quote for full balance to discover fees
-        // We need to create a dummy mint quote first to get an invoice
-        let dummy_mint_quote = target_wallet
-            .mint_quote(PaymentMethod::BOLT11, Some(source_balance), None, None)
-            .await?;
-        let probe_melt_quote = source_wallet
-            .melt_quote(
-                PaymentMethod::BOLT11,
-                dummy_mint_quote.request.clone(),
-                None,
-                None,
-            )
-            .await?;
-
-        // Step 2: Calculate actual receive amount (balance - fees)
-        let receive_amount = source_balance
-            .checked_sub(probe_melt_quote.fee_reserve)
-            .ok_or(Error::InsufficientFunds)?;
-
-        if receive_amount == Amount::ZERO {
-            return Err(Error::InsufficientFunds);
-        }
-
-        // Step 3: Create final mint quote for the net amount
-        let final_mint_quote = target_wallet
-            .mint_quote(PaymentMethod::BOLT11, Some(receive_amount), None, None)
-            .await?;
-
-        // Step 4: Create final melt quote with the new invoice
-        let final_melt_quote = source_wallet
-            .melt_quote(
-                PaymentMethod::BOLT11,
-                final_mint_quote.request.clone(),
-                None,
-                None,
-            )
-            .await?;
-
-        Ok((final_mint_quote, final_melt_quote))
-    }
-
-    /// Get all pending send operations across all mints
-    ///
-    /// Returns a list of (MintUrl, Uuid) tuples for all pending sends.
-    #[instrument(skip(self))]
-    pub async fn get_pending_sends(&self) -> Result<Vec<(MintUrl, Uuid)>, Error> {
-        let mut pending_sends = Vec::new();
-
-        for (mint_url, wallet) in self.wallets.read().await.iter() {
-            let wallet_pending = wallet.get_pending_sends().await?;
-            for id in wallet_pending {
-                pending_sends.push((mint_url.clone(), id));
-            }
-        }
-
-        Ok(pending_sends)
-    }
-
-    /// Revoke a pending send operation for a specific mint
-    ///
-    /// Attempts to reclaim the funds by swapping the proofs back to the wallet.
-    /// If successful, the saga is deleted.
-    #[instrument(skip(self))]
-    pub async fn revoke_send(
-        &self,
-        mint_url: MintUrl,
-        operation_id: Uuid,
-    ) -> Result<Amount, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.revoke_send(operation_id).await
-    }
-
-    /// Check status of a pending send operation for a specific mint
-    ///
-    /// Checks if the token has been claimed by the recipient.
-    /// If claimed, the saga is finalized (deleted).
-    /// Returns true if claimed, false if still pending.
-    #[instrument(skip(self))]
-    pub async fn check_send_status(
-        &self,
-        mint_url: MintUrl,
-        operation_id: Uuid,
-    ) -> Result<bool, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.check_send_status(operation_id).await
-    }
-
-    /// Execute the actual transfer using the prepared quotes
-    async fn execute_transfer(
-        &self,
-        source_wallet: &Wallet,
-        target_wallet: &Wallet,
-        final_mint_quote: &MintQuote,
-        final_melt_quote: &crate::wallet::types::MeltQuote,
-    ) -> Result<(FinalizedMelt, Amount), Error> {
-        // Step 1: Subscribe to mint quote updates before melting
-        let mut subscription = target_wallet
-            .subscribe(super::WalletSubscription::Bolt11MintQuoteState(vec![
-                final_mint_quote.id.clone(),
-            ]))
-            .await?;
-
-        // Step 2: Melt from source wallet using the final melt quote
-        let prepared = source_wallet
-            .prepare_melt(&final_melt_quote.id, std::collections::HashMap::new())
-            .await?;
-        let melted = prepared.confirm().await?;
-
-        // Step 3: Wait for payment confirmation via subscription
-        tracing::debug!(
-            "Waiting for Lightning payment confirmation (max {} seconds) for transfer from {} to {}",
-            TRANSFER_PAYMENT_TIMEOUT_SECS,
-            source_wallet.mint_url,
-            target_wallet.mint_url
-        );
-
-        // Wait for payment notification with overall timeout
-        let timeout_duration = tokio::time::Duration::from_secs(TRANSFER_PAYMENT_TIMEOUT_SECS);
-
-        loop {
-            match tokio::time::timeout(timeout_duration, subscription.recv()).await {
-                Ok(Some(notification)) => {
-                    // Check if this is a mint quote response with paid state
-                    if let crate::nuts::nut17::NotificationPayload::MintQuoteBolt11Response(
-                        quote_response,
-                    ) = notification.deref()
-                    {
-                        if quote_response.state == QuoteState::Paid {
-                            // Quote is paid, now mint the tokens
-                            target_wallet
-                                .mint(
-                                    &final_mint_quote.id,
-                                    crate::amount::SplitTarget::default(),
-                                    None,
-                                )
-                                .await?;
-                            break;
-                        }
-                    }
-                }
-                Ok(None) => {
-                    // Subscription closed
-                    tracing::warn!("Subscription closed while waiting for mint quote payment");
-                    return Err(Error::TransferTimeout {
-                        source_mint: source_wallet.mint_url.to_string(),
-                        target_mint: target_wallet.mint_url.to_string(),
-                        amount: final_mint_quote.amount.unwrap_or(Amount::ZERO),
-                    });
-                }
-                Err(_) => {
-                    // Overall timeout reached
-                    tracing::warn!(
-                        "Transfer timed out after {} seconds waiting for Lightning payment confirmation",
-                        TRANSFER_PAYMENT_TIMEOUT_SECS
-                    );
-                    return Err(Error::TransferTimeout {
-                        source_mint: source_wallet.mint_url.to_string(),
-                        target_mint: target_wallet.mint_url.to_string(),
-                        amount: final_mint_quote.amount.unwrap_or(Amount::ZERO),
-                    });
-                }
-            }
-        }
-
-        let actual_receive_amount = final_mint_quote.amount.unwrap_or(Amount::ZERO);
-        Ok((melted, actual_receive_amount))
-    }
-
-    /// Transfer funds from multiple source wallets to target mint in parallel
-    async fn transfer_parallel(
-        &self,
-        target_mint_url: &MintUrl,
-        total_amount: Amount,
-        source_mints: Vec<(MintUrl, Amount)>,
-    ) -> Result<(), Error> {
-        let mut remaining_amount = total_amount;
-        let mut transfer_tasks = Vec::new();
-
-        // Create transfer tasks for each source wallet
-        for (source_mint_url, available_balance) in source_mints {
-            if remaining_amount == Amount::ZERO {
-                break;
-            }
-
-            let transfer_amount = std::cmp::min(remaining_amount, available_balance);
-            remaining_amount -= transfer_amount;
-
-            let self_clone = self.clone();
-            let source_mint_url = source_mint_url.clone();
-            let target_mint_url = target_mint_url.clone();
-
-            // Spawn parallel transfer task
-            let task = spawn(async move {
-                self_clone
-                    .transfer(
-                        &source_mint_url,
-                        &target_mint_url,
-                        TransferMode::ExactReceive(transfer_amount),
-                    )
-                    .await
-                    .map(|result| result.amount_received)
-            });
-
-            transfer_tasks.push(task);
-        }
-
-        // Wait for all transfers to complete
-        let mut total_transferred = Amount::ZERO;
-        for task in transfer_tasks {
-            match task.await {
-                Ok(Ok(amount)) => {
-                    total_transferred += amount;
-                }
-                Ok(Err(e)) => {
-                    tracing::error!("Transfer failed: {}", e);
-                    return Err(e);
-                }
-                Err(e) => {
-                    tracing::error!("Transfer task panicked: {}", e);
-                    return Err(Error::Internal);
-                }
-            }
-        }
-
-        // Check if we transferred less than expected (accounting for fees)
-        // We don't return an error here as fees are expected
-        if total_transferred < total_amount {
-            let fee_paid = total_amount - total_transferred;
-            tracing::info!(
-                "Transfer completed with fees: requested {}, received {}, total fees {}",
-                total_amount,
-                total_transferred,
-                fee_paid
-            );
-        }
-
-        Ok(())
-    }
-
-    /// Mint quote for wallet
-    #[instrument(skip(self, method))]
-    pub async fn mint_quote<T>(
-        &self,
-        mint_url: &MintUrl,
-        method: T,
-        amount: Option<Amount>,
-        description: Option<String>,
-        extra: Option<String>,
-    ) -> Result<MintQuote, Error>
-    where
-        T: Into<PaymentMethod>,
-    {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.mint_quote(method, amount, description, extra).await
-    }
-
-    /// Refresh a specific mint quote status from the mint.
-    /// Updates local store with current state from mint.
-    /// Does NOT mint tokens - use wallet.mint() to mint a specific quote.
-    #[instrument(skip(self))]
-    pub async fn refresh_mint_quote(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-    ) -> Result<MintQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        // Refresh the quote state from the mint
-        wallet.refresh_mint_quote_status(quote_id).await?;
-
-        // Get the updated quote from local storage
-        let quote = wallet
-            .localstore
-            .get_mint_quote(quote_id)
-            .await
-            .map_err(Error::Database)?
-            .ok_or(Error::UnknownQuote)?;
-
-        Ok(quote)
-    }
-
-    /// Fetch a mint quote from the mint and store it locally
-    ///
-    /// Works with all payment methods (Bolt11, Bolt12, and custom payment methods).
-    ///
-    /// # Arguments
-    /// * `mint_url` - The URL of the mint
-    /// * `quote_id` - The ID of the quote to fetch
-    /// * `payment_method` - The payment method for the quote. Required if the quote
-    ///   is not already stored locally. If the quote exists locally, the stored
-    ///   payment method will be used and this parameter is ignored.
-    #[instrument(skip(self))]
-    pub async fn fetch_mint_quote(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-        payment_method: Option<PaymentMethod>,
-    ) -> Result<MintQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.fetch_mint_quote(quote_id, payment_method).await
-    }
-
-    /// Mint tokens at a specific mint
-    #[instrument(skip(self))]
-    pub async fn mint(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-        amount_split_target: SplitTarget,
-        spending_conditions: Option<SpendingConditions>,
-    ) -> Result<Proofs, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet
-            .mint(quote_id, amount_split_target, spending_conditions)
-            .await
-    }
-
-    /// Refresh all unissued mint quote states
-    /// Does NOT mint - use mint_unissued_quotes() for that
-    #[instrument(skip(self))]
-    pub async fn refresh_all_mint_quotes(
-        &self,
-        mint_url: Option<MintUrl>,
-    ) -> Result<Vec<MintQuote>, Error> {
-        let mut all_quotes = Vec::new();
-        match mint_url {
-            Some(mint_url) => {
-                let wallets = self.wallets.read().await;
-                let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?;
-
-                all_quotes = wallet.refresh_all_mint_quotes().await?;
-            }
-            None => {
-                for (_, wallet) in self.wallets.read().await.iter() {
-                    let quotes = wallet.refresh_all_mint_quotes().await?;
-                    all_quotes.extend(quotes);
-                }
-            }
-        }
-
-        Ok(all_quotes)
-    }
-
-    /// Refresh states and mint all unissued quotes
-    /// Returns total amount minted across all wallets
-    #[instrument(skip(self))]
-    pub async fn mint_unissued_quotes(&self, mint_url: Option<MintUrl>) -> Result<Amount, Error> {
-        let mut total_amount = Amount::ZERO;
-        match mint_url {
-            Some(mint_url) => {
-                let wallets = self.wallets.read().await;
-                let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?;
-
-                total_amount = wallet.mint_unissued_quotes().await?;
-            }
-            None => {
-                for (_, wallet) in self.wallets.read().await.iter() {
-                    let amount = wallet.mint_unissued_quotes().await?;
-                    total_amount += amount;
-                }
-            }
-        }
-
-        Ok(total_amount)
-    }
-
-    /// Set the active mint for NpubCash integration
-    ///
-    /// This method sets the active mint for NpubCash in the key-value store.
-    /// Since all wallets share the same seed (and thus the same Nostr identity),
-    /// only one mint should be active for NpubCash at a time to avoid conflicts.
-    #[cfg(feature = "npubcash")]
-    #[instrument(skip(self))]
-    pub async fn set_active_npubcash_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
-        use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
-
-        self.localstore
-            .kv_write(
-                NPUBCASH_KV_NAMESPACE,
-                "",
-                ACTIVE_MINT_KEY,
-                mint_url.to_string().as_bytes(),
-            )
-            .await?;
-
-        Ok(())
-    }
-
-    /// Get the active mint for NpubCash integration
-    ///
-    /// Returns the currently active mint URL from the key-value store, if any.
-    #[cfg(feature = "npubcash")]
-    #[instrument(skip(self))]
-    pub async fn get_active_npubcash_mint(&self) -> Result<Option<MintUrl>, Error> {
-        use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
-
-        let value = self
-            .localstore
-            .kv_read(NPUBCASH_KV_NAMESPACE, "", ACTIVE_MINT_KEY)
-            .await?;
-
-        match value {
-            Some(bytes) => {
-                let url_str = String::from_utf8(bytes)
-                    .map_err(|_| Error::Custom("Invalid UTF-8 in active mint URL".into()))?;
-                let mint_url = MintUrl::from_str(&url_str)?;
-                Ok(Some(mint_url))
-            }
-            None => Ok(None),
-        }
-    }
-
-    /// Enable NpubCash integration on a specific mint
-    ///
-    /// This sets up NpubCash authentication and registers the mint URL with the
-    /// NpubCash server. It also sets this mint as the active NpubCash mint.
-    #[cfg(feature = "npubcash")]
-    #[instrument(skip(self))]
-    pub async fn enable_npubcash(
-        &self,
-        mint_url: MintUrl,
-        npubcash_url: String,
-    ) -> Result<(), Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.enable_npubcash(npubcash_url).await?;
-        drop(wallets);
-
-        self.set_active_npubcash_mint(mint_url).await?;
-        Ok(())
-    }
-
-    /// Get the Nostr keys used for NpubCash authentication
-    ///
-    /// Since all wallets share the same seed, they all have the same Nostr identity.
-    /// This returns the keys from any wallet in the MultiMintWallet.
-    #[cfg(feature = "npubcash")]
-    pub async fn get_npubcash_keys(&self) -> Result<nostr_sdk::Keys, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.values().next().ok_or(Error::Custom(
-            "No wallets available to get NpubCash keys".into(),
-        ))?;
-        wallet.get_npubcash_keys()
-    }
-
-    /// Sync quotes from NpubCash for the active mint
-    ///
-    /// Fetches quotes from the NpubCash server and filters them to only return
-    /// quotes for the currently active mint.
-    #[cfg(feature = "npubcash")]
-    #[instrument(skip(self))]
-    pub async fn sync_npubcash_quotes(
-        &self,
-    ) -> Result<Vec<crate::wallet::types::MintQuote>, Error> {
-        let active_mint = self
-            .get_active_npubcash_mint()
-            .await?
-            .ok_or(Error::Custom("No active NpubCash mint set".into()))?;
-
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&active_mint).ok_or(Error::UnknownMint {
-            mint_url: active_mint.to_string(),
-        })?;
-
-        let all_quotes = wallet.sync_npubcash_quotes().await?;
-
-        // Filter to only quotes for the active mint
-        let filtered_quotes: Vec<_> = all_quotes
-            .into_iter()
-            .filter(|q| q.mint_url == active_mint)
-            .collect();
-
-        Ok(filtered_quotes)
-    }
-
-    /// Mint ecash from a paid NpubCash quote
-    ///
-    /// This mints ecash from a quote on the active NpubCash mint.
-    #[cfg(feature = "npubcash")]
-    #[instrument(skip(self))]
-    pub async fn mint_npubcash_quote(
-        &self,
-        quote_id: &str,
-        split_target: SplitTarget,
-    ) -> Result<Proofs, Error> {
-        let active_mint = self
-            .get_active_npubcash_mint()
-            .await?
-            .ok_or(Error::Custom("No active NpubCash mint set".into()))?;
-
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&active_mint).ok_or(Error::UnknownMint {
-            mint_url: active_mint.to_string(),
-        })?;
-
-        wallet.mint(quote_id, split_target, None).await
-    }
-
-    /// Create a stream that continuously polls NpubCash and yields proofs as payments arrive
-    ///
-    /// This provides a reactive way to handle incoming NpubCash payments. The stream will:
-    /// 1. Poll NpubCash for new paid quotes
-    /// 2. Automatically mint them using the active mint
-    /// 3. Yield the result (MintQuote, Proofs)
-    ///
-    /// # Arguments
-    ///
-    /// * `split_target` - How to split the minted proofs
-    /// * `spending_conditions` - Optional spending conditions for the minted proofs
-    /// * `poll_interval` - How often to check for new quotes
-    #[cfg(feature = "npubcash")]
-    pub fn npubcash_proof_stream(
-        &self,
-        split_target: SplitTarget,
-        spending_conditions: Option<SpendingConditions>,
-        poll_interval: std::time::Duration,
-    ) -> crate::wallet::streams::npubcash::NpubCashProofStream {
-        crate::wallet::streams::npubcash::NpubCashProofStream::new(
-            self.clone(),
-            poll_interval,
-            split_target,
-            spending_conditions,
-        )
-    }
-
-    /// Wait for a mint quote to be paid and automatically mint the proofs
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint URL where the quote was created
-    /// * `quote_id` - The quote ID to wait for
-    /// * `split_target` - How to split the minted proofs
-    /// * `spending_conditions` - Optional spending conditions for the minted proofs
-    /// * `timeout` - Maximum time to wait for the quote to be paid
-    #[cfg(not(target_arch = "wasm32"))]
-    #[instrument(skip(self))]
-    pub async fn wait_for_mint_quote(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-        split_target: SplitTarget,
-        spending_conditions: Option<SpendingConditions>,
-        timeout: std::time::Duration,
-    ) -> Result<Proofs, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        // Get the mint quote from local storage
-        let quote = wallet
-            .localstore
-            .get_mint_quote(quote_id)
-            .await
-            .map_err(Error::Database)?
-            .ok_or(Error::UnknownQuote)?;
-
-        // Wait for the quote to be paid and mint the proofs
-        wallet
-            .wait_and_mint_quote(quote, split_target, spending_conditions, timeout)
-            .await
-    }
-
-    /// Receive token with multi-mint options
-    ///
-    /// This method can:
-    /// - Receive tokens from trusted mints (already added to the wallet)
-    /// - Optionally receive from untrusted mints by adding them to the wallet
-    /// - Optionally transfer tokens from untrusted mints to a trusted mint (and remove the untrusted mint)
-    ///
-    /// # Examples
-    /// ```no_run
-    /// # use cdk::wallet::{MultiMintWallet, MultiMintReceiveOptions};
-    /// # use cdk::mint_url::MintUrl;
-    /// # async fn example(wallet: MultiMintWallet) -> Result<(), Box<dyn std::error::Error>> {
-    /// // Receive from a trusted mint
-    /// let token = "cashuAey...";
-    /// let amount = wallet
-    ///     .receive(token, MultiMintReceiveOptions::default())
-    ///     .await?;
-    ///
-    /// // Receive from untrusted mint and add it to the wallet
-    /// let options = MultiMintReceiveOptions::default().allow_untrusted(true);
-    /// let amount = wallet.receive(token, options).await?;
-    ///
-    /// // Receive from untrusted mint, transfer to trusted mint, then remove untrusted mint
-    /// let trusted_mint: MintUrl = "https://trusted.mint".parse()?;
-    /// let options = MultiMintReceiveOptions::default().transfer_to_mint(Some(trusted_mint));
-    /// let amount = wallet.receive(token, options).await?;
-    /// # Ok(())
-    /// # }
-    /// ```
-    #[instrument(skip_all)]
-    pub async fn receive(
-        &self,
-        encoded_token: &str,
-        opts: MultiMintReceiveOptions,
-    ) -> Result<Amount, Error> {
-        let token_data = Token::from_str(encoded_token)?;
-        let unit = token_data.unit().unwrap_or_default();
-
-        // Ensure the token uses the same currency unit as this wallet
-        if unit != self.unit {
-            return Err(Error::MultiMintCurrencyUnitMismatch {
-                expected: self.unit.clone(),
-                found: unit,
-            });
-        }
-
-        let mint_url = token_data.mint_url()?;
-        let is_trusted = self.has_mint(&mint_url).await;
-
-        // If mint is not trusted and we don't allow untrusted mints, error
-        if !is_trusted && !opts.allow_untrusted {
-            return Err(Error::UnknownMint {
-                mint_url: mint_url.to_string(),
-            });
-        }
-
-        // If mint is untrusted and we need to transfer, ensure we have a target mint
-        let should_transfer = !is_trusted && opts.transfer_to_mint.is_some();
-
-        // Add the untrusted mint temporarily if needed
-        if !is_trusted {
-            self.add_mint(mint_url.clone()).await?;
-        }
-
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        // We need the keysets information to properly convert from token proof to proof
-        let keysets_info = match self
-            .localstore
-            .get_mint_keysets(token_data.mint_url()?)
-            .await?
-        {
-            Some(keysets_info) => keysets_info,
-            // Hit the keysets endpoint if we don't have the keysets for this Mint
-            None => wallet.load_mint_keysets().await?,
-        };
-        let proofs = token_data.proofs(&keysets_info)?;
-
-        let mut amount_received = Amount::ZERO;
-
-        match wallet
-            .receive_proofs(
-                proofs,
-                opts.receive_options,
-                token_data.memo().clone(),
-                Some(encoded_token.to_string()),
-            )
-            .await
-        {
-            Ok(amount) => {
-                amount_received += amount;
-            }
-            Err(err) => {
-                // If we added the mint temporarily for transfer only, remove it before returning error
-                if !is_trusted && opts.transfer_to_mint.is_some() {
-                    drop(wallets);
-                    self.remove_mint(&mint_url).await;
-                }
-                return Err(err);
-            }
-        }
-
-        drop(wallets);
-
-        // If we should transfer to a trusted mint, do so now
-        if should_transfer {
-            if let Some(target_mint) = opts.transfer_to_mint {
-                // Ensure target mint exists and is trusted
-                if !self.has_mint(&target_mint).await {
-                    // Clean up untrusted mint if we're only using it for transfer
-                    self.remove_mint(&mint_url).await;
-                    return Err(Error::UnknownMint {
-                        mint_url: target_mint.to_string(),
-                    });
-                }
-
-                // Transfer the entire balance from the untrusted mint to the target mint
-                // Use FullBalance mode for efficient transfer of all funds
-                let transfer_result = self
-                    .transfer(&mint_url, &target_mint, TransferMode::FullBalance)
-                    .await;
-
-                // Handle transfer result - log details but don't fail if balance was zero
-                match transfer_result {
-                    Ok(result) => {
-                        if result.amount_sent > Amount::ZERO {
-                            tracing::info!(
-                                "Transferred {} sats from untrusted mint {} to trusted mint {} (received: {}, fees: {})",
-                                result.amount_sent,
-                                mint_url,
-                                target_mint,
-                                result.amount_received,
-                                result.fees_paid
-                            );
-                        }
-                    }
-                    Err(Error::InsufficientFunds) => {
-                        // No balance to transfer, which is fine
-                        tracing::debug!("No balance to transfer from untrusted mint {}", mint_url);
-                    }
-                    Err(e) => return Err(e),
-                }
-
-                // Remove the untrusted mint after transfer
-                self.remove_mint(&mint_url).await;
-            }
-        }
-        // Note: If allow_untrusted is true but no transfer is requested,
-        // the untrusted mint is kept in the wallet (as intended)
-
-        Ok(amount_received)
-    }
-
-    /// Restore
-    #[instrument(skip(self))]
-    pub async fn restore(&self, mint_url: &MintUrl) -> Result<Restored, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.restore().await
-    }
-
-    /// Verify token matches p2pk conditions
-    #[instrument(skip(self, token))]
-    pub async fn verify_token_p2pk(
-        &self,
-        token: &Token,
-        conditions: SpendingConditions,
-    ) -> Result<(), Error> {
-        let mint_url = token.mint_url()?;
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.verify_token_p2pk(token, conditions).await
-    }
-
-    /// Verifies all proofs in token have valid dleq proof
-    #[instrument(skip(self, token))]
-    pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
-        let mint_url = token.mint_url()?;
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(&mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.verify_token_dleq(token).await
-    }
-
-    /// Create a melt quote for a specific mint
-    #[instrument(skip(self, method, request))]
-    pub async fn melt_quote<T, R>(
-        &self,
-        mint_url: &MintUrl,
-        method: T,
-        request: R,
-        options: Option<MeltOptions>,
-        extra: Option<String>,
-    ) -> Result<MeltQuote, Error>
-    where
-        T: Into<PaymentMethod> + std::fmt::Debug,
-        R: std::fmt::Display,
-    {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.melt_quote(method, request, options, extra).await
-    }
-
-    /// Melt (pay invoice) from a specific mint using a quote ID
-    ///
-    /// For more control over fees, use `prepare_melt()` instead.
-    #[instrument(skip(self))]
-    pub async fn melt_with_mint(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-    ) -> Result<FinalizedMelt, Error> {
-        let wallet = {
-            let wallets = self.wallets.read().await;
-            wallets
-                .get(mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone()
-        };
-
-        let prepared = wallet
-            .prepare_melt(quote_id, std::collections::HashMap::new())
-            .await?;
-        prepared.confirm().await
-    }
-
-    /// Melt specific proofs from a specific mint using a quote ID
-    ///
-    /// This method allows melting proofs that may not be in the wallet's database,
-    /// similar to how `receive_proofs` handles external proofs. The proofs will be
-    /// added to the database and used for the melt operation.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for the melt operation
-    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
-    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
-    ///
-    /// # Returns
-    ///
-    /// A `FinalizedMelt` result containing the payment details and any change proofs
-    #[instrument(skip(self, proofs))]
-    pub async fn melt_proofs(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-        proofs: Proofs,
-    ) -> Result<FinalizedMelt, Error> {
-        let wallet = {
-            let wallets = self.wallets.read().await;
-            wallets
-                .get(mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone()
-        };
-
-        let prepared = wallet
-            .prepare_melt_proofs(quote_id, proofs, std::collections::HashMap::new())
-            .await?;
-        prepared.confirm().await
-    }
-
-    /// Check a specific melt quote status
-    #[instrument(skip(self))]
-    pub async fn check_melt_quote(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-    ) -> Result<MeltQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        // Check the quote state from the mint
-        wallet.check_melt_quote_status(quote_id).await?;
-
-        // Get the updated quote from local storage
-        let quote = wallet
-            .localstore
-            .get_melt_quote(quote_id)
-            .await
-            .map_err(Error::Database)?
-            .ok_or(Error::UnknownQuote)?;
-
-        Ok(quote)
-    }
-
-    /// Create MPP (Multi-Path Payment) melt quotes from multiple mints
-    ///
-    /// This function allows manual specification of which mints and amounts to use for MPP.
-    /// Returns a vector of (MintUrl, MeltQuote) pairs.
-    #[instrument(skip(self, bolt11))]
-    pub async fn mpp_melt_quote(
-        &self,
-        bolt11: String,
-        mint_amounts: Vec<(MintUrl, Amount)>,
-    ) -> Result<Vec<(MintUrl, crate::wallet::types::MeltQuote)>, Error> {
-        let mut quotes = Vec::new();
-        let mut tasks = Vec::new();
-
-        // Spawn parallel tasks to get quotes from each mint
-        for (mint_url, amount) in mint_amounts {
-            let wallets = self.wallets.read().await;
-            let wallet = wallets
-                .get(&mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone();
-            drop(wallets);
-
-            let bolt11_clone = bolt11.clone();
-            let mint_url_clone = mint_url.clone();
-
-            // Convert amount to millisats for MeltOptions
-            let amount_msat = u64::from(amount) * 1000;
-            let options = Some(MeltOptions::new_mpp(amount_msat));
-
-            let task = spawn(async move {
-                let quote = wallet
-                    .melt_quote(PaymentMethod::BOLT11, bolt11_clone, options, None)
-                    .await;
-                (mint_url_clone, quote)
-            });
-
-            tasks.push(task);
-        }
-
-        // Collect all quote results
-        for task in tasks {
-            match task.await {
-                Ok((mint_url, Ok(quote))) => {
-                    quotes.push((mint_url, quote));
-                }
-                Ok((mint_url, Err(e))) => {
-                    tracing::error!("Failed to get melt quote from {}: {}", mint_url, e);
-                    return Err(e);
-                }
-                Err(e) => {
-                    tracing::error!("Task failed: {}", e);
-                    return Err(Error::Internal);
-                }
-            }
-        }
-
-        Ok(quotes)
-    }
-
-    /// Execute MPP melts using previously obtained quotes
-    #[instrument(skip(self))]
-    pub async fn mpp_melt(
-        &self,
-        quotes: Vec<(MintUrl, String)>, // (mint_url, quote_id)
-    ) -> Result<Vec<(MintUrl, FinalizedMelt)>, Error> {
-        let mut results = Vec::new();
-        let mut tasks = Vec::new();
-
-        for (mint_url, quote_id) in quotes {
-            let wallets = self.wallets.read().await;
-            let wallet = wallets
-                .get(&mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone();
-            drop(wallets);
-
-            let mint_url_clone = mint_url.clone();
-
-            let task = spawn(async move {
-                let result = async {
-                    let prepared = wallet
-                        .prepare_melt(&quote_id, std::collections::HashMap::new())
-                        .await?;
-                    prepared.confirm().await
-                }
-                .await;
-                (mint_url_clone, result)
-            });
-
-            tasks.push(task);
-        }
-
-        // Collect all melt results
-        for task in tasks {
-            match task.await {
-                Ok((mint_url, Ok(melted))) => {
-                    results.push((mint_url, melted));
-                }
-                Ok((mint_url, Err(e))) => {
-                    tracing::error!("Failed to melt from {}: {}", mint_url, e);
-                    return Err(e);
-                }
-                Err(e) => {
-                    tracing::error!("Task failed: {}", e);
-                    return Err(Error::Internal);
-                }
-            }
-        }
-
-        Ok(results)
-    }
-
-    /// Prepare a melt operation from a specific mint
-    ///
-    /// Returns a [`MultiMintPreparedMelt`] that holds an `Arc<Wallet>` and can be
-    /// confirmed later by calling `.confirm()`.
-    ///
-    /// # Example
-    /// ```ignore
-    /// let quote = wallet.melt_quote(&mint_url, "lnbc...", None).await?;
-    /// let prepared = wallet.prepare_melt(&mint_url, &quote.id, HashMap::new()).await?;
-    /// // Inspect the prepared melt...
-    /// println!("Fee: {}", prepared.total_fee());
-    /// // Then confirm or cancel
-    /// let confirmed = prepared.confirm().await?;
-    /// ```
-    #[instrument(skip(self, metadata))]
-    pub async fn prepare_melt(
-        &self,
-        mint_url: &MintUrl,
-        quote_id: &str,
-        metadata: std::collections::HashMap<String, String>,
-    ) -> Result<MultiMintPreparedMelt, Error> {
-        // Clone the Arc<Wallet> and release the lock immediately
-        let wallet = {
-            let wallets = self.wallets.read().await;
-            wallets
-                .get(mint_url)
-                .ok_or(Error::UnknownMint {
-                    mint_url: mint_url.to_string(),
-                })?
-                .clone()
-        };
-
-        // Call prepare_melt on the wallet (lock is released)
-        let prepared = wallet.prepare_melt(quote_id, metadata.clone()).await?;
-
-        // Extract data into MultiMintPreparedMelt
-        // Clone the Arc again since `prepared` borrows from `wallet`
-        Ok(MultiMintPreparedMelt {
-            wallet: Arc::clone(&wallet),
-            operation_id: prepared.operation_id(),
-            quote: prepared.quote().clone(),
-            proofs: prepared.proofs().clone(),
-            proofs_to_swap: prepared.proofs_to_swap().clone(),
-            swap_fee: prepared.swap_fee(),
-            input_fee: prepared.input_fee(),
-            input_fee_without_swap: prepared.input_fee_without_swap(),
-            metadata,
-        })
-    }
-
-    /// Melt (pay invoice) with automatic wallet selection (deprecated, use specific mint functions for better control)
-    ///
-    /// Automatically selects the best wallet to pay from based on:
-    /// - Available balance
-    /// - Fees
-    ///
-    /// # Examples
-    /// ```no_run
-    /// # use cdk::wallet::MultiMintWallet;
-    /// # use cdk::Amount;
-    /// # use std::sync::Arc;
-    /// # async fn example(wallet: Arc<MultiMintWallet>) -> Result<(), Box<dyn std::error::Error>> {
-    /// // Pay a lightning invoice from any mint with sufficient balance
-    /// let invoice = "lnbc100n1p...";
-    ///
-    /// let result = wallet.melt(invoice, None, None).await?;
-    /// println!("Paid {} sats, fee was {} sats", result.amount(), result.fee_paid());
-    /// # Ok(())
-    /// # }
-    /// ```
-    #[instrument(skip(self, bolt11))]
-    pub async fn melt(
-        &self,
-        bolt11: &str,
-        options: Option<MeltOptions>,
-        max_fee: Option<Amount>,
-    ) -> Result<FinalizedMelt, Error> {
-        // Parse the invoice to get the amount
-        let invoice = bolt11
-            .parse::<crate::Bolt11Invoice>()
-            .map_err(Error::Invoice)?;
-
-        let amount = invoice
-            .amount_milli_satoshis()
-            .map(|msats| Amount::from(msats / 1000))
-            .ok_or(Error::InvoiceAmountUndefined)?;
-
-        let wallets = self.wallets.read().await;
-        let mut eligible_wallets = Vec::new();
-
-        for (mint_url, wallet) in wallets.iter() {
-            let balance = wallet.total_balance().await?;
-            if balance >= amount {
-                eligible_wallets.push((mint_url.clone(), wallet.clone()));
-            }
-        }
-
-        if eligible_wallets.is_empty() {
-            return Err(Error::InsufficientFunds);
-        }
-
-        // Try to get quotes from eligible wallets and select the best one
-        let mut best_quote = None;
-        let mut best_wallet = None;
-
-        for (_, wallet) in eligible_wallets.iter() {
-            match wallet
-                .melt_quote(PaymentMethod::BOLT11, bolt11.to_string(), options, None)
-                .await
-            {
-                Ok(quote) => {
-                    if let Some(max_fee) = max_fee {
-                        if quote.fee_reserve > max_fee {
-                            continue;
-                        }
-                    }
-
-                    if best_quote.is_none() {
-                        best_quote = Some(quote);
-                        best_wallet = Some(wallet.clone());
-                    } else if let Some(ref existing_quote) = best_quote {
-                        if quote.fee_reserve < existing_quote.fee_reserve {
-                            best_quote = Some(quote);
-                            best_wallet = Some(wallet.clone());
-                        }
-                    }
-                }
-                Err(_) => continue,
-            }
-        }
-
-        if let (Some(quote), Some(wallet)) = (best_quote, best_wallet) {
-            let prepared = wallet
-                .prepare_melt(&quote.id, std::collections::HashMap::new())
-                .await?;
-            return prepared.confirm().await;
-        }
-
-        Err(Error::InsufficientFunds)
-    }
-
-    /// Swap proofs with automatic wallet selection
-    #[instrument(skip(self))]
-    pub async fn swap(
-        &self,
-        amount: Option<Amount>,
-        conditions: Option<SpendingConditions>,
-    ) -> Result<Option<Proofs>, Error> {
-        // Find a wallet that has proofs
-        let wallets = self.wallets.read().await;
-
-        for (_, wallet) in wallets.iter() {
-            let balance = wallet.total_balance().await?;
-            if balance > Amount::ZERO {
-                // Try to swap with this wallet
-                let proofs = wallet.get_unspent_proofs().await?;
-                if !proofs.is_empty() {
-                    return wallet
-                        .swap(amount, SplitTarget::default(), proofs, conditions, false)
-                        .await;
-                }
-            }
-        }
-
-        Err(Error::InsufficientFunds)
-    }
-
-    /// Consolidate proofs from multiple wallets into fewer, larger proofs
-    /// This can help reduce the number of proofs and optimize wallet performance
-    #[instrument(skip(self))]
-    pub async fn consolidate(&self) -> Result<Amount, Error> {
-        let mut total_consolidated = Amount::ZERO;
-        let wallets = self.wallets.read().await;
-
-        for (mint_url, wallet) in wallets.iter() {
-            // Get all unspent proofs for this wallet
-            let proofs = wallet.get_unspent_proofs().await?;
-            if proofs.len() > 1 {
-                // Consolidate by swapping all proofs for a single set
-                let proofs_amount = proofs.total_amount()?;
-
-                // Swap for optimized proof set
-                match wallet
-                    .swap(
-                        Some(proofs_amount),
-                        SplitTarget::default(),
-                        proofs,
-                        None,
-                        false,
-                    )
-                    .await
-                {
-                    Ok(_) => {
-                        total_consolidated += proofs_amount;
-                    }
-                    Err(e) => {
-                        tracing::warn!(
-                            "Failed to consolidate proofs for mint {:?}: {}",
-                            mint_url,
-                            e
-                        );
-                    }
-                }
-            }
-        }
-
-        Ok(total_consolidated)
-    }
-
-    /// Mint blind auth tokens for a specific mint
-    ///
-    /// This is a convenience method that calls the underlying wallet's mint_blind_auth.
-    #[instrument(skip_all)]
-    pub async fn mint_blind_auth(
-        &self,
-        mint_url: &MintUrl,
-        amount: Amount,
-    ) -> Result<Proofs, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.mint_blind_auth(amount).await
-    }
-
-    /// Get unspent auth proofs for a specific mint
-    ///
-    /// This is a convenience method that calls the underlying wallet's get_unspent_auth_proofs.
-    #[instrument(skip_all)]
-    pub async fn get_unspent_auth_proofs(
-        &self,
-        mint_url: &MintUrl,
-    ) -> Result<Vec<cdk_common::AuthProof>, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.get_unspent_auth_proofs().await
-    }
-
-    /// Set Clear Auth Token (CAT) for authentication at a specific mint
-    ///
-    /// This is a convenience method that calls the underlying wallet's set_cat.
-    #[instrument(skip_all)]
-    pub async fn set_cat(&self, mint_url: &MintUrl, cat: String) -> Result<(), Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.set_cat(cat).await
-    }
-
-    /// Set refresh token for authentication at a specific mint
-    ///
-    /// This is a convenience method that calls the underlying wallet's set_refresh_token.
-    #[instrument(skip_all)]
-    pub async fn set_refresh_token(
-        &self,
-        mint_url: &MintUrl,
-        refresh_token: String,
-    ) -> Result<(), Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.set_refresh_token(refresh_token).await
-    }
-
-    /// Refresh CAT token for a specific mint
-    ///
-    /// This is a convenience method that calls the underlying wallet's refresh_access_token.
-    #[instrument(skip(self))]
-    pub async fn refresh_access_token(&self, mint_url: &MintUrl) -> Result<(), Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.refresh_access_token().await
-    }
-
-    /// Query mint for current mint information
-    ///
-    /// This is a convenience method that calls the underlying wallet's fetch_mint_info.
-    #[instrument(skip(self))]
-    pub async fn fetch_mint_info(
-        &self,
-        mint_url: &MintUrl,
-    ) -> Result<Option<crate::nuts::MintInfo>, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.fetch_mint_info().await
-    }
-
-    /// Get mint info for all wallets
-    ///
-    /// This method loads the mint info for each wallet in the MultiMintWallet
-    /// and returns a map of mint URLs to their corresponding mint info.
-    ///
-    /// Uses cached mint info when available, only fetching from the mint if the cache
-    /// has expired.
-    #[instrument(skip(self))]
-    pub async fn get_all_mint_info(
-        &self,
-    ) -> Result<BTreeMap<MintUrl, crate::nuts::MintInfo>, Error> {
-        let mut mint_infos = BTreeMap::new();
-
-        for (mint_url, wallet) in self.wallets.read().await.iter() {
-            let mint_info = wallet.load_mint_info().await?;
-            mint_infos.insert(mint_url.clone(), mint_info);
-        }
-
-        Ok(mint_infos)
-    }
-
-    /// Melt Quote for BIP353 human-readable address
-    ///
-    /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
-    /// and then creates a melt quote for that offer at the specified mint.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `bip353_address` - Human-readable address in the format "user@domain.com"
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    ///
-    /// # Returns
-    ///
-    /// A `MeltQuote` that can be used to execute the payment
-    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
-    #[instrument(skip(self, amount_msat))]
-    pub async fn melt_bip353_quote(
-        &self,
-        mint_url: &MintUrl,
-        bip353_address: &str,
-        amount_msat: impl Into<Amount>,
-    ) -> Result<crate::wallet::types::MeltQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.melt_bip353_quote(bip353_address, amount_msat).await
-    }
-
-    /// Melt Quote for Lightning address
-    ///
-    /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
-    /// and then creates a melt quote for that invoice at the specified mint.
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `lightning_address` - Lightning address in the format "user@domain.com"
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    ///
-    /// # Returns
-    ///
-    /// A `MeltQuote` that can be used to execute the payment
-    #[instrument(skip(self, amount_msat))]
-    pub async fn melt_lightning_address_quote(
-        &self,
-        mint_url: &MintUrl,
-        lightning_address: &str,
-        amount_msat: impl Into<Amount>,
-    ) -> Result<crate::wallet::types::MeltQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet
-            .melt_lightning_address_quote(lightning_address, amount_msat)
-            .await
-    }
-
-    /// Get a melt quote for a human-readable address
-    ///
-    /// This method accepts a human-readable address that could be either a BIP353 address
-    /// or a Lightning address. It intelligently determines which to try based on mint support:
-    ///
-    /// 1. If the mint supports Bolt12, it tries BIP353 first
-    /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails
-    /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address
-    /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
-    ///
-    /// # Arguments
-    ///
-    /// * `mint_url` - The mint to use for creating the melt quote
-    /// * `address` - Human-readable address (BIP353 or Lightning address)
-    /// * `amount_msat` - Amount to pay in millisatoshis
-    #[cfg(all(feature = "bip353", feature = "wallet", not(target_arch = "wasm32")))]
-    #[instrument(skip(self, amount_msat))]
-    pub async fn melt_human_readable_quote(
-        &self,
-        mint_url: &MintUrl,
-        address: &str,
-        amount_msat: impl Into<Amount>,
-    ) -> Result<crate::wallet::types::MeltQuote, Error> {
-        let wallets = self.wallets.read().await;
-        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
-            mint_url: mint_url.to_string(),
-        })?;
-
-        wallet.melt_human_readable_quote(address, amount_msat).await
-    }
-}
-
-impl Drop for MultiMintWallet {
-    fn drop(&mut self) {
-        self.seed.zeroize();
-    }
-}
-
-/// Multi-Mint Receive Options
-///
-/// Controls how tokens are received, especially from untrusted mints
-#[derive(Debug, Clone, Default)]
-pub struct MultiMintReceiveOptions {
-    /// Whether to allow receiving from untrusted (not yet added) mints
-    pub allow_untrusted: bool,
-    /// Mint to transfer tokens to from untrusted mints (None means keep in original mint)
-    pub transfer_to_mint: Option<MintUrl>,
-    /// Base receive options to apply to the wallet receive
-    pub receive_options: ReceiveOptions,
-}
-
-impl MultiMintReceiveOptions {
-    /// Create new default options
-    pub fn new() -> Self {
-        Default::default()
-    }
-
-    /// Allow receiving from untrusted mints
-    pub fn allow_untrusted(mut self, allow: bool) -> Self {
-        self.allow_untrusted = allow;
-        self
-    }
-
-    /// Set mint to transfer tokens to from untrusted mints
-    pub fn transfer_to_mint(mut self, mint_url: Option<MintUrl>) -> Self {
-        self.transfer_to_mint = mint_url;
-        self
-    }
-
-    /// Set the base receive options for the wallet operation
-    pub fn receive_options(mut self, options: ReceiveOptions) -> Self {
-        self.receive_options = options;
-        self
-    }
-}
-
-/// Multi-Mint Send Options
-///
-/// Controls transfer behavior when the target mint doesn't have sufficient balance
-#[derive(Debug, Clone, Default)]
-pub struct MultiMintSendOptions {
-    /// Whether to allow transferring funds from other mints to the sending mint
-    /// if the sending mint doesn't have sufficient balance
-    pub allow_transfer: bool,
-    /// Maximum amount to transfer from other mints (optional limit)
-    pub max_transfer_amount: Option<Amount>,
-    /// Specific mints allowed for transfers (empty means all mints allowed)
-    pub allowed_mints: Vec<MintUrl>,
-    /// Specific mints to exclude from transfers
-    pub excluded_mints: Vec<MintUrl>,
-    /// Base send options to apply to the wallet send
-    pub send_options: SendOptions,
-}
-
-impl MultiMintSendOptions {
-    /// Create new default options
-    pub fn new() -> Self {
-        Default::default()
-    }
-
-    /// Enable transferring funds from other mints if needed
-    pub fn allow_transfer(mut self, allow: bool) -> Self {
-        self.allow_transfer = allow;
-        self
-    }
-
-    /// Set maximum amount to transfer from other mints
-    pub fn max_transfer_amount(mut self, amount: Amount) -> Self {
-        self.max_transfer_amount = Some(amount);
-        self
-    }
-
-    /// Add a mint to the allowed list for transfers
-    pub fn allow_mint(mut self, mint_url: MintUrl) -> Self {
-        self.allowed_mints.push(mint_url);
-        self
-    }
-
-    /// Set all allowed mints for transfers
-    pub fn allowed_mints(mut self, mints: Vec<MintUrl>) -> Self {
-        self.allowed_mints = mints;
-        self
-    }
-
-    /// Add a mint to exclude from transfers
-    pub fn exclude_mint(mut self, mint_url: MintUrl) -> Self {
-        self.excluded_mints.push(mint_url);
-        self
-    }
-
-    /// Set all excluded mints for transfers
-    pub fn excluded_mints(mut self, mints: Vec<MintUrl>) -> Self {
-        self.excluded_mints = mints;
-        self
-    }
-
-    /// Set the base send options for the wallet operation
-    pub fn send_options(mut self, options: SendOptions) -> Self {
-        self.send_options = options;
-        self
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use std::sync::Arc;
-
-    use cdk_common::database::WalletDatabase;
-
-    use super::*;
-
-    async fn create_test_multi_wallet() -> MultiMintWallet {
-        let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
-            cdk_sqlite::wallet::memory::empty()
-                .await
-                .expect("Failed to create in-memory database"),
-        );
-        let seed = [0u8; 64];
-        MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
-            .await
-            .expect("Failed to create MultiMintWallet")
-    }
-
-    #[tokio::test]
-    async fn test_total_balance_empty() {
-        let multi_wallet = create_test_multi_wallet().await;
-        let balance = multi_wallet.total_balance().await.unwrap();
-        assert_eq!(balance, Amount::ZERO);
-    }
-
-    #[tokio::test]
-    async fn test_send_insufficient_funds() {
-        use std::str::FromStr;
-
-        let multi_wallet = create_test_multi_wallet().await;
-        let mint_url = MintUrl::from_str("https://mint1.example.com").unwrap();
-        let options = MultiMintSendOptions::new();
-
-        let result = multi_wallet
-            .send(mint_url, Amount::from(1000), options)
-            .await;
-
-        assert!(result.is_err());
-    }
-
-    #[tokio::test]
-    async fn test_consolidate_empty() {
-        let multi_wallet = create_test_multi_wallet().await;
-        let result = multi_wallet.consolidate().await.unwrap();
-        assert_eq!(result, Amount::ZERO);
-    }
-
-    #[tokio::test]
-    async fn test_multi_mint_wallet_creation() {
-        let multi_wallet = create_test_multi_wallet().await;
-        assert!(multi_wallet.wallets.try_read().is_ok());
-    }
-
-    #[tokio::test]
-    async fn test_multi_mint_send_options() {
-        use std::str::FromStr;
-
-        let mint1 = MintUrl::from_str("https://mint1.example.com").unwrap();
-        let mint2 = MintUrl::from_str("https://mint2.example.com").unwrap();
-        let mint3 = MintUrl::from_str("https://mint3.example.com").unwrap();
-
-        let options = MultiMintSendOptions::new()
-            .allow_transfer(true)
-            .max_transfer_amount(Amount::from(500))
-            .allow_mint(mint1.clone())
-            .allow_mint(mint2.clone())
-            .exclude_mint(mint3.clone())
-            .send_options(SendOptions::default());
-
-        assert!(options.allow_transfer);
-        assert_eq!(options.max_transfer_amount, Some(Amount::from(500)));
-        assert_eq!(options.allowed_mints, vec![mint1, mint2]);
-        assert_eq!(options.excluded_mints, vec![mint3]);
-    }
-
-    #[tokio::test]
-    async fn test_get_mint_keysets_unknown_mint() {
-        use std::str::FromStr;
-
-        let multi_wallet = create_test_multi_wallet().await;
-        let mint_url = MintUrl::from_str("https://unknown-mint.example.com").unwrap();
-
-        // Should error when trying to get keysets for a mint that hasn't been added
-        let result = multi_wallet.get_mint_keysets(&mint_url).await;
-        assert!(result.is_err());
-
-        match result {
-            Err(Error::UnknownMint { mint_url: url }) => {
-                assert!(url.contains("unknown-mint.example.com"));
-            }
-            _ => panic!("Expected UnknownMint error"),
-        }
-    }
-
-    #[tokio::test]
-    async fn test_multi_mint_receive_options() {
-        use std::str::FromStr;
-
-        let mint_url = MintUrl::from_str("https://trusted.mint.example.com").unwrap();
-
-        // Test default options
-        let default_opts = MultiMintReceiveOptions::default();
-        assert!(!default_opts.allow_untrusted);
-        assert!(default_opts.transfer_to_mint.is_none());
-
-        // Test builder pattern
-        let opts = MultiMintReceiveOptions::new()
-            .allow_untrusted(true)
-            .transfer_to_mint(Some(mint_url.clone()));
-
-        assert!(opts.allow_untrusted);
-        assert_eq!(opts.transfer_to_mint, Some(mint_url));
-    }
-
-    #[tokio::test]
-    async fn test_get_token_data_unknown_mint() {
-        use std::str::FromStr;
-
-        let multi_wallet = create_test_multi_wallet().await;
-
-        // Create a token from a mint that isn't in the wallet
-        // This is a valid token structure pointing to an unknown mint
-        let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
-        let token = Token::from_str(token_str).unwrap();
-
-        // Should error because the mint (localhost:3338) hasn't been added
-        let result = multi_wallet.get_token_data(&token).await;
-        assert!(result.is_err());
-
-        match result {
-            Err(Error::UnknownMint { mint_url }) => {
-                assert!(mint_url.contains("localhost:3338"));
-            }
-            _ => panic!("Expected UnknownMint error"),
-        }
-    }
-
-    #[test]
-    fn test_token_data_struct() {
-        use std::str::FromStr;
-
-        let mint_url = MintUrl::from_str("https://example.mint.com").unwrap();
-        let proofs = vec![];
-        let memo = Some("Test memo".to_string());
-
-        let token_data = TokenData {
-            value: Amount::ZERO,
-            mint_url: mint_url.clone(),
-            proofs: proofs.clone(),
-            memo: memo.clone(),
-            unit: CurrencyUnit::Sat,
-            redeem_fee: None,
-        };
-
-        assert_eq!(token_data.mint_url, mint_url);
-        assert_eq!(token_data.proofs.len(), 0);
-        assert_eq!(token_data.memo, memo);
-
-        // Test with no memo
-        let token_data_no_memo = TokenData {
-            value: Amount::ZERO,
-            mint_url: mint_url.clone(),
-            proofs: vec![],
-            memo: None,
-            unit: CurrencyUnit::Sat,
-            redeem_fee: None,
-        };
-        assert!(token_data_no_memo.memo.is_none());
-    }
-
-    #[tokio::test]
-    async fn test_wallet_config_metadata_ttl() {
-        let ttl = std::time::Duration::from_secs(12345);
-        let config = WalletConfig::new().with_metadata_cache_ttl(Some(ttl));
-        assert_eq!(config.metadata_cache_ttl, Some(ttl));
-    }
-}

+ 4 - 3
crates/cdk/src/wallet/nostr_backup.rs

@@ -9,7 +9,7 @@ use nostr_sdk::prelude::*;
 use nostr_sdk::{Client as NostrClient, Filter, Keys};
 use tracing::instrument;
 
-use super::multi_mint_wallet::MultiMintWallet;
+use super::wallet_repository::WalletRepository;
 use crate::error::Error;
 use crate::mint_url::MintUrl;
 use crate::nuts::nut27::{
@@ -86,7 +86,7 @@ pub struct RestoreResult {
     pub mints_added: usize,
 }
 
-impl MultiMintWallet {
+impl WalletRepository {
     /// Derive the Nostr keys used for mint backup from the wallet seed
     ///
     /// These keys can be used to identify and decrypt backup events.
@@ -236,7 +236,8 @@ impl MultiMintWallet {
             for mint_url in &backup.mints {
                 if !self.has_mint(mint_url).await {
                     // Ignore errors for individual mints to continue restoring others
-                    if self.add_mint(mint_url.clone()).await.is_ok() {
+                    // add_wallet fetches mint info and creates wallets for all supported units
+                    if self.add_wallet(mint_url.clone()).await.is_ok() {
                         mints_added += 1;
                     }
                 }

+ 63 - 31
crates/cdk/src/wallet/payment_request.rs

@@ -23,8 +23,8 @@ use crate::nuts::nut11::{Conditions, SigFlag, SpendingConditions};
 use crate::nuts::nut18::Nut10SecretRequest;
 use crate::nuts::{CurrencyUnit, Nut10Secret, Transport};
 #[cfg(feature = "nostr")]
-use crate::wallet::MultiMintReceiveOptions;
-use crate::wallet::{MultiMintWallet, SendOptions};
+use crate::wallet::ReceiveOptions;
+use crate::wallet::{SendOptions, WalletRepository};
 use crate::Wallet;
 
 impl Wallet {
@@ -237,8 +237,8 @@ pub struct NostrWaitInfo {
     pub pubkey: nostr_sdk::PublicKey,
 }
 
-impl MultiMintWallet {
-    /// Pay a NUT-18 PaymentRequest using the MultiMintWallet.
+impl WalletRepository {
+    /// Pay a NUT-18 PaymentRequest using the WalletRepository.
     ///
     /// This method handles paying a payment request by selecting an appropriate mint:
     /// - If `mint_url` is provided, it verifies the payment request accepts that mint
@@ -277,6 +277,9 @@ impl MultiMintWallet {
         // Get the list of mints accepted by the payment request (None means any mint is accepted)
         let accepted_mints = payment_request.mints.as_ref();
 
+        // Get the unit from the payment request, defaulting to Sat
+        let unit = payment_request.unit.clone().unwrap_or(CurrencyUnit::Sat);
+
         // Select the wallet to use for payment
         let selected_wallet = if let Some(specified_mint) = &mint_url {
             // User specified a mint - verify it's accepted by the payment request
@@ -289,22 +292,23 @@ impl MultiMintWallet {
                 }
             }
 
-            // Get the wallet for the specified mint
-            self.get_wallet(specified_mint)
-                .await
-                .ok_or_else(|| Error::UnknownMint {
-                    mint_url: specified_mint.to_string(),
-                })?
+            // Get the wallet for the specified mint and unit
+            self.get_wallet(specified_mint, &unit).await?
         } else {
             // No mint specified - find the best matching mint with highest balance
             let balances = self.get_balances().await?;
             let mut best_wallet: Option<Arc<Wallet>> = None;
             let mut best_balance = Amount::ZERO;
 
-            for (mint_url, balance) in balances.iter() {
+            for (wallet_key, balance) in balances.iter() {
+                // Only consider wallets with matching unit
+                if wallet_key.unit != unit {
+                    continue;
+                }
+
                 // Check if this mint is accepted by the payment request
                 let is_accepted = match accepted_mints {
-                    Some(accepted) => accepted.contains(mint_url),
+                    Some(accepted) => accepted.contains(&wallet_key.mint_url),
                     None => true, // No mints specified means any mint is accepted
                 };
 
@@ -314,14 +318,16 @@ impl MultiMintWallet {
 
                 // Check balance meets requirements and is best so far
                 if *balance >= amount && *balance > best_balance {
-                    if let Some(wallet) = self.get_wallet(mint_url).await {
+                    if let Ok(wallet) = self.get_wallet(&wallet_key.mint_url, &unit).await {
                         best_balance = *balance;
-                        best_wallet = Some(wallet);
+                        best_wallet = Some(Arc::new(wallet));
                     }
                 }
             }
 
-            best_wallet.ok_or(Error::InsufficientFunds)?
+            best_wallet
+                .map(|w| (*w).clone())
+                .ok_or(Error::InsufficientFunds)?
         };
 
         // Use the selected wallet to pay the request
@@ -463,12 +469,15 @@ impl MultiMintWallet {
         params: CreateRequestParams,
     ) -> Result<(PaymentRequest, Option<NostrWaitInfo>), Error> {
         // Collect available mints for the selected unit
-        let mints = self
+        // Filter by the requested unit and extract unique mint URLs
+        let requested_unit = CurrencyUnit::from_str(&params.unit)?;
+        let mints: Vec<MintUrl> = self
             .get_balances()
             .await?
             .keys()
-            .cloned()
-            .collect::<Vec<_>>();
+            .filter(|key| key.unit == requested_unit)
+            .map(|key| key.mint_url.clone())
+            .collect();
 
         // Transports
         let transport_type = params.transport.to_lowercase();
@@ -565,12 +574,15 @@ impl MultiMintWallet {
         params: CreateRequestParams,
     ) -> Result<PaymentRequest, Error> {
         // Collect available mints for the selected unit
-        let mints = self
+        // Filter by the requested unit and extract unique mint URLs
+        let requested_unit = CurrencyUnit::from_str(&params.unit)?;
+        let mints: Vec<MintUrl> = self
             .get_balances()
             .await?
             .keys()
-            .cloned()
-            .collect::<Vec<_>>();
+            .filter(|key| key.unit == requested_unit)
+            .map(|key| key.mint_url.clone())
+            .collect();
 
         // Transports
         let transport_type = params.transport.to_lowercase();
@@ -637,19 +649,28 @@ impl MultiMintWallet {
             match item {
                 Ok(payload) => {
                     let token = crate::nuts::Token::new(
-                        payload.mint,
+                        payload.mint.clone(),
                         payload.proofs,
                         payload.memo,
-                        payload.unit,
+                        payload.unit.clone(),
                     );
 
-                    let amount = self
-                        .receive(&token.to_string(), MultiMintReceiveOptions::default())
+                    // Get or create wallet for the token's mint
+                    let unit = payload.unit.clone();
+                    let wallet = match self.get_wallet(&payload.mint, &unit).await {
+                        Ok(w) => w,
+                        Err(_) => self.create_wallet(payload.mint.clone(), unit, None).await?,
+                    };
+
+                    // Receive using the individual wallet
+                    let token_str = token.to_string();
+                    let received = wallet
+                        .receive(&token_str, ReceiveOptions::default())
                         .await?;
 
                     // Stop after first successful receipt
                     cancel.cancel();
-                    return Ok(amount);
+                    return Ok(received);
                 }
                 Err(_) => {
                     // Keep listening on parse errors; if you prefer fail-fast, return the error
@@ -703,17 +724,28 @@ impl MultiMintWallet {
                         match serde_json::from_str::<PaymentRequestPayload>(&rumor.content) {
                             Ok(payload) => {
                                 let token = crate::nuts::Token::new(
-                                    payload.mint,
+                                    payload.mint.clone(),
                                     payload.proofs,
                                     payload.memo,
-                                    payload.unit,
+                                    payload.unit.clone(),
                                 );
 
-                                let amount = self
-                                    .receive(&token.to_string(), MultiMintReceiveOptions::default())
+                                // Get or create wallet for the token's mint
+                                let unit = payload.unit.clone();
+                                let wallet = match self.get_wallet(&payload.mint, &unit).await {
+                                    Ok(w) => w,
+                                    Err(_) => {
+                                        self.create_wallet(payload.mint.clone(), unit, None).await?
+                                    }
+                                };
+
+                                // Receive using the individual wallet
+                                let token_str = token.to_string();
+                                let received = wallet
+                                    .receive(&token_str, ReceiveOptions::default())
                                     .await?;
 
-                                return Ok(amount);
+                                return Ok(received);
                             }
                             Err(_) => {
                                 // Ignore malformed payloads and continue listening

+ 4 - 6
crates/cdk/src/wallet/streams/npubcash.rs

@@ -14,8 +14,8 @@ use tokio_util::sync::CancellationToken;
 
 use crate::error::Error;
 use crate::nuts::{Proofs, SpendingConditions};
-use crate::wallet::multi_mint_wallet::MultiMintWallet;
 use crate::wallet::types::MintQuote;
+use crate::wallet::wallet_repository::WalletRepository;
 use crate::wallet::Wallet;
 
 /// Stream that continuously polls NpubCash and yields proofs as payments arrive
@@ -28,7 +28,7 @@ pub struct NpubCashProofStream {
 impl NpubCashProofStream {
     /// Create a new NpubCash proof stream
     pub fn new(
-        wallet: MultiMintWallet,
+        wallet: WalletRepository,
         poll_interval: Duration,
         split_target: SplitTarget,
         spending_conditions: Option<SpendingConditions>,
@@ -55,10 +55,8 @@ impl NpubCashProofStream {
                                         tracing::info!("Minting NpubCash quote {}...", quote_id);
 
                                         let result = async {
-                                            // Get wallet for this quote's mint
-                                            let wallet_instance = wallet.get_wallet(&mint_url).await.ok_or(Error::UnknownMint {
-                                                mint_url: mint_url.to_string(),
-                                            })?;
+                                            // Get wallet for this quote's mint and unit
+                                            let wallet_instance = wallet.get_wallet(&mint_url, &quote.unit).await?;
 
                                             let proofs = wallet_instance
                                                 .mint(

+ 970 - 0
crates/cdk/src/wallet/wallet_repository.rs

@@ -0,0 +1,970 @@
+//! Wallet Repository
+//!
+//! Simple container that manages [`Wallet`] instances by mint URL.
+
+use std::collections::BTreeMap;
+#[cfg(feature = "npubcash")]
+use std::str::FromStr;
+use std::sync::Arc;
+
+use cdk_common::database;
+use cdk_common::database::WalletDatabase;
+use cdk_common::wallet::WalletKey;
+use tokio::sync::RwLock;
+use tracing::instrument;
+use zeroize::Zeroize;
+
+use super::builder::WalletBuilder;
+use super::{Error, MintConnector};
+use crate::mint_url::MintUrl;
+use crate::nuts::CurrencyUnit;
+#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+use crate::wallet::mint_connector::transport::tor_transport::TorAsync;
+use crate::Wallet;
+
+/// Data extracted from a token
+///
+/// Contains the mint URL, proofs, and metadata from a parsed token.
+#[derive(Debug, Clone)]
+pub struct TokenData {
+    /// The mint URL from the token
+    pub mint_url: MintUrl,
+    /// The proofs contained in the token
+    pub proofs: cdk_common::Proofs,
+    /// The memo from the token, if present
+    pub memo: Option<String>,
+    /// Value of token
+    pub value: cdk_common::Amount,
+    /// Unit of token
+    pub unit: CurrencyUnit,
+    /// Fee to redeem
+    ///
+    /// If the token is for a mint that we do not know, we cannot get the fee.
+    /// To avoid just erroring and still allow decoding, this is an option.
+    /// None does not mean there is no fee, it means we do not know the fee.
+    pub redeem_fee: Option<cdk_common::Amount>,
+}
+
+/// Configuration for individual wallets within WalletRepository
+#[derive(Clone, Default, Debug)]
+pub struct WalletConfig {
+    /// Custom mint connector implementation
+    pub mint_connector: Option<Arc<dyn super::MintConnector + Send + Sync>>,
+    /// Custom auth connector implementation
+    pub auth_connector: Option<Arc<dyn super::auth::AuthMintConnector + Send + Sync>>,
+    /// Target number of proofs to maintain at each denomination
+    pub target_proof_count: Option<usize>,
+    /// Metadata cache TTL
+    ///
+    /// The TTL determines how often the wallet checks the mint for new keysets and information.
+    ///
+    /// If `None`, the cache will never expire and the wallet will use cached data indefinitely
+    /// (unless manually refreshed).
+    ///
+    /// The default value is 1 hour (3600 seconds).
+    pub metadata_cache_ttl: Option<std::time::Duration>,
+}
+
+impl WalletConfig {
+    /// Create a new empty WalletConfig
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Set custom mint connector
+    pub fn with_mint_connector(
+        mut self,
+        connector: Arc<dyn super::MintConnector + Send + Sync>,
+    ) -> Self {
+        self.mint_connector = Some(connector);
+        self
+    }
+
+    /// Set custom auth connector
+    pub fn with_auth_connector(
+        mut self,
+        connector: Arc<dyn super::auth::AuthMintConnector + Send + Sync>,
+    ) -> Self {
+        self.auth_connector = Some(connector);
+        self
+    }
+
+    /// Set target proof count
+    pub fn with_target_proof_count(mut self, count: usize) -> Self {
+        self.target_proof_count = Some(count);
+        self
+    }
+
+    /// Set metadata cache TTL
+    ///
+    /// The TTL determines how often the wallet checks the mint for new keysets and information.
+    ///
+    /// If `None`, the cache will never expire and the wallet will use cached data indefinitely
+    /// (unless manually refreshed).
+    ///
+    /// The default value is 1 hour (3600 seconds).
+    pub fn with_metadata_cache_ttl(mut self, ttl: Option<std::time::Duration>) -> Self {
+        self.metadata_cache_ttl = ttl;
+        self
+    }
+}
+
+/// Builder for creating [`WalletRepository`] instances
+///
+/// # Example
+/// ```no_run
+/// # use std::sync::Arc;
+/// # use cdk::wallet::WalletRepositoryBuilder;
+/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
+/// let localstore = Arc::new(cdk_sqlite::wallet::memory::empty().await?);
+/// let seed = [0u8; 64];
+/// let wallet_repo = WalletRepositoryBuilder::new()
+///     .localstore(localstore)
+///     .seed(seed)
+///     .build()
+///     .await?;
+/// # Ok(())
+/// # }
+/// ```
+pub struct WalletRepositoryBuilder {
+    localstore: Option<Arc<dyn WalletDatabase<database::Error> + Send + Sync>>,
+    seed: Option<[u8; 64]>,
+    proxy_config: Option<url::Url>,
+    #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+    use_tor: bool,
+}
+
+impl std::fmt::Debug for WalletRepositoryBuilder {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("WalletRepositoryBuilder")
+            .field("localstore", &self.localstore.as_ref().map(|_| "..."))
+            .field("seed", &"[REDACTED]")
+            .field("proxy_config", &self.proxy_config)
+            .finish()
+    }
+}
+
+impl Default for WalletRepositoryBuilder {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl WalletRepositoryBuilder {
+    /// Create a new builder
+    pub fn new() -> Self {
+        Self {
+            localstore: None,
+            seed: None,
+            proxy_config: None,
+            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+            use_tor: false,
+        }
+    }
+
+    /// Set the storage backend
+    pub fn localstore(
+        mut self,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
+    ) -> Self {
+        self.localstore = Some(localstore);
+        self
+    }
+
+    /// Set the wallet seed
+    pub fn seed(mut self, seed: [u8; 64]) -> Self {
+        self.seed = Some(seed);
+        self
+    }
+
+    /// Set the proxy URL for HTTP clients
+    pub fn proxy_url(mut self, proxy_url: url::Url) -> Self {
+        self.proxy_config = Some(proxy_url);
+        self
+    }
+
+    /// Enable Tor transport
+    #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+    pub fn tor(mut self) -> Self {
+        self.use_tor = true;
+        self
+    }
+
+    /// Build the WalletRepository and load existing wallets from database
+    pub async fn build(self) -> Result<WalletRepository, Error> {
+        let localstore = self
+            .localstore
+            .ok_or(Error::Custom("localstore is required".into()))?;
+        let seed = self.seed.ok_or(Error::Custom("seed is required".into()))?;
+
+        let wallet = WalletRepository {
+            localstore,
+            seed,
+            wallets: Arc::new(RwLock::new(BTreeMap::new())),
+            proxy_config: self.proxy_config,
+            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+            shared_tor_transport: if self.use_tor {
+                Some(TorAsync::new())
+            } else {
+                None
+            },
+        };
+
+        wallet.load_wallets().await?;
+        Ok(wallet)
+    }
+}
+
+/// Repository for managing Wallet instances by mint URL and currency unit
+///
+/// Simple container that bootstraps wallets from database and provides
+/// access to individual Wallet instances. Each wallet is uniquely identified
+/// by the combination of mint URL and currency unit.
+#[derive(Clone)]
+pub struct WalletRepository {
+    /// Storage backend
+    localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
+    seed: [u8; 64],
+    /// Wallets indexed by (mint URL, currency unit)
+    wallets: Arc<RwLock<BTreeMap<WalletKey, Wallet>>>,
+    /// Proxy configuration for HTTP clients (optional)
+    proxy_config: Option<url::Url>,
+    /// Shared Tor transport to be cloned into each TorHttpClient (if enabled)
+    #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+    shared_tor_transport: Option<TorAsync>,
+}
+
+impl std::fmt::Debug for WalletRepository {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("WalletRepository").finish_non_exhaustive()
+    }
+}
+
+impl WalletRepository {
+    /// Get the wallet seed
+    pub fn seed(&self) -> &[u8; 64] {
+        &self.seed
+    }
+
+    /// Get wallet for a mint URL and currency unit
+    ///
+    /// Returns an error if no wallet exists for the given mint URL and unit combination.
+    #[instrument(skip(self))]
+    pub async fn get_wallet(
+        &self,
+        mint_url: &MintUrl,
+        unit: &CurrencyUnit,
+    ) -> Result<Wallet, Error> {
+        let key = WalletKey::new(mint_url.clone(), unit.clone());
+        self.wallets
+            .read()
+            .await
+            .get(&key)
+            .cloned()
+            .ok_or_else(|| Error::UnknownWallet(key))
+    }
+
+    /// Get all wallets for a specific mint URL (any currency unit)
+    #[instrument(skip(self))]
+    pub async fn get_wallets_for_mint(&self, mint_url: &MintUrl) -> Vec<Wallet> {
+        self.wallets
+            .read()
+            .await
+            .iter()
+            .filter(|(key, _)| &key.mint_url == mint_url)
+            .map(|(_, wallet)| wallet.clone())
+            .collect()
+    }
+
+    /// Check if a specific wallet exists (mint URL + unit combination)
+    #[instrument(skip(self))]
+    pub async fn has_wallet(&self, mint_url: &MintUrl, unit: &CurrencyUnit) -> bool {
+        let key = WalletKey::new(mint_url.clone(), unit.clone());
+        self.wallets.read().await.contains_key(&key)
+    }
+
+    /// Add wallets for a mint to the repository
+    ///
+    /// Fetches the mint info to discover all supported currency units and creates
+    /// a wallet for each unit. Returns all created wallets.
+    #[instrument(skip(self))]
+    pub async fn add_wallet(&self, mint_url: MintUrl) -> Result<Vec<Wallet>, Error> {
+        self.add_wallet_with_config(mint_url, None).await
+    }
+
+    /// Add wallets for a mint to the repository with a custom configuration
+    ///
+    /// Fetches the mint info to discover all supported currency units and creates
+    /// a wallet for each unit with the given configuration. Returns all created wallets.
+    #[instrument(skip(self))]
+    pub async fn add_wallet_with_config(
+        &self,
+        mint_url: MintUrl,
+        config: Option<WalletConfig>,
+    ) -> Result<Vec<Wallet>, Error> {
+        // Fetch mint info to get supported units
+        let mint_info = self.fetch_mint_info(&mint_url).await?;
+        let supported_units = mint_info.supported_units();
+
+        if supported_units.is_empty() {
+            return Err(Error::Custom(
+                "Mint does not support any currency units".into(),
+            ));
+        }
+
+        let mut wallets = Vec::new();
+        for unit in supported_units {
+            // Skip if wallet already exists for this unit
+            if self.has_wallet(&mint_url, unit).await {
+                if let Ok(existing) = self.get_wallet(&mint_url, unit).await {
+                    wallets.push(existing);
+                }
+                continue;
+            }
+
+            let wallet = self
+                .create_wallet(mint_url.clone(), unit.clone(), config.clone())
+                .await?;
+            wallets.push(wallet);
+        }
+
+        Ok(wallets)
+    }
+
+    /// Update configuration for an existing mint and unit
+    ///
+    /// This re-creates the wallet with the new configuration.
+    #[instrument(skip(self))]
+    pub async fn set_mint_config(
+        &self,
+        mint_url: MintUrl,
+        unit: CurrencyUnit,
+        config: WalletConfig,
+    ) -> Result<Wallet, Error> {
+        // Re-create wallet with new config
+        self.create_wallet(mint_url, unit, Some(config)).await
+    }
+
+    /// Create and add a new wallet for a mint URL and currency unit
+    /// Returns the created wallet
+    #[instrument(skip(self))]
+    pub async fn create_wallet(
+        &self,
+        mint_url: MintUrl,
+        unit: CurrencyUnit,
+        config: Option<WalletConfig>,
+    ) -> Result<Wallet, Error> {
+        let wallet = self
+            .create_wallet_internal(mint_url.clone(), unit.clone(), config.as_ref())
+            .await?;
+
+        // Insert into wallets map using WalletKey
+        let key = WalletKey::new(mint_url, unit);
+        let mut wallets = self.wallets.write().await;
+        wallets.insert(key, wallet.clone());
+
+        Ok(wallet)
+    }
+
+    /// Remove a wallet from the in-memory repository
+    ///
+    /// This only removes the wallet from the in-memory map. It does not remove
+    /// the mint from the database. Use the database directly if you need to
+    /// explicitly remove persisted mint data.
+    #[instrument(skip(self))]
+    pub async fn remove_wallet(
+        &self,
+        mint_url: MintUrl,
+        currency_unit: CurrencyUnit,
+    ) -> Result<(), Error> {
+        let key = WalletKey::new(mint_url, currency_unit);
+        let mut wallets = self.wallets.write().await;
+
+        if !wallets.contains_key(&key) {
+            return Err(Error::UnknownWallet(key));
+        }
+
+        wallets.remove(&key);
+        Ok(())
+    }
+
+    /// Get all wallets
+    #[instrument(skip(self))]
+    pub async fn get_wallets(&self) -> Vec<Wallet> {
+        self.wallets.read().await.values().cloned().collect()
+    }
+
+    /// Check if any wallet exists for a mint (regardless of currency unit)
+    #[instrument(skip(self))]
+    pub async fn has_mint(&self, mint_url: &MintUrl) -> bool {
+        self.wallets
+            .read()
+            .await
+            .keys()
+            .any(|key| &key.mint_url == mint_url)
+    }
+    /// Get balances for all wallets
+    ///
+    /// Returns a map of (mint URL, currency unit) to balance for each wallet in the repository.
+    #[instrument(skip(self))]
+    pub async fn get_balances(&self) -> Result<BTreeMap<WalletKey, cdk_common::Amount>, Error> {
+        let wallets = self.wallets.read().await;
+        let mut balances = BTreeMap::new();
+
+        for (key, wallet) in wallets.iter() {
+            let balance = wallet.total_balance().await?;
+            balances.insert(key.clone(), balance);
+        }
+
+        Ok(balances)
+    }
+    /// Get total balance across all wallets, grouped by currency unit
+    ///
+    /// Returns a map of currency unit to total balance for that unit across all mints.
+    #[instrument(skip(self))]
+    pub async fn total_balance(&self) -> Result<BTreeMap<CurrencyUnit, cdk_common::Amount>, Error> {
+        let balances = self.get_balances().await?;
+        let mut by_unit: BTreeMap<CurrencyUnit, cdk_common::Amount> = BTreeMap::new();
+        for (key, amount) in balances {
+            let entry = by_unit.entry(key.unit).or_insert(cdk_common::Amount::ZERO);
+            *entry += amount;
+        }
+        Ok(by_unit)
+    }
+
+    /// Fetch mint info from a mint URL
+    ///
+    /// Creates a temporary HTTP client to fetch the mint info.
+    /// This is useful to discover supported currency units before adding a mint.
+    pub async fn fetch_mint_info(
+        &self,
+        mint_url: &MintUrl,
+    ) -> Result<crate::nuts::MintInfo, Error> {
+        // Create an HTTP client based on the repository configuration
+        let client: Arc<dyn MintConnector + Send + Sync> =
+            if let Some(proxy_url) = &self.proxy_config {
+                Arc::new(
+                    crate::wallet::HttpClient::with_proxy(
+                        mint_url.clone(),
+                        proxy_url.clone(),
+                        None,
+                        true,
+                    )
+                    .unwrap_or_else(|_| crate::wallet::HttpClient::new(mint_url.clone(), None)),
+                )
+            } else {
+                #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+                if let Some(tor) = &self.shared_tor_transport {
+                    let transport = tor.clone();
+                    Arc::new(crate::wallet::TorHttpClient::with_transport(
+                        mint_url.clone(),
+                        transport,
+                        None,
+                    ))
+                } else {
+                    Arc::new(crate::wallet::HttpClient::new(mint_url.clone(), None))
+                }
+
+                #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
+                {
+                    Arc::new(crate::wallet::HttpClient::new(mint_url.clone(), None))
+                }
+            };
+
+        client.get_mint_info().await
+    }
+
+    /// Internal: Create wallet with optional custom configuration
+    ///
+    /// Priority order for configuration:
+    /// 1. Custom connector from config (if provided)
+    /// 2. Global settings (proxy/Tor)
+    /// 3. Default HttpClient
+    async fn create_wallet_internal(
+        &self,
+        mint_url: MintUrl,
+        unit: CurrencyUnit,
+        config: Option<&WalletConfig>,
+    ) -> Result<Wallet, Error> {
+        // Check if custom connector is provided in config
+        if let Some(cfg) = config {
+            if let Some(custom_connector) = &cfg.mint_connector {
+                // Use custom connector with WalletBuilder
+                let mut builder = WalletBuilder::new()
+                    .mint_url(mint_url.clone())
+                    .unit(unit.clone())
+                    .localstore(self.localstore.clone())
+                    .seed(self.seed)
+                    .target_proof_count(cfg.target_proof_count.unwrap_or(3))
+                    .shared_client(custom_connector.clone());
+
+                if let Some(ttl) = cfg.metadata_cache_ttl {
+                    builder = builder.set_metadata_cache_ttl(Some(ttl));
+                }
+
+                return builder.build();
+            }
+        }
+
+        // Fall back to existing logic: proxy/Tor/default
+        let target_proof_count = config.and_then(|c| c.target_proof_count).unwrap_or(3);
+        let metadata_cache_ttl = config.and_then(|c| c.metadata_cache_ttl);
+
+        let wallet = if let Some(proxy_url) = &self.proxy_config {
+            // Create wallet with proxy-configured client
+            let client = crate::wallet::HttpClient::with_proxy(
+                mint_url.clone(),
+                proxy_url.clone(),
+                None,
+                true,
+            )
+            .unwrap_or_else(|_| crate::wallet::HttpClient::new(mint_url.clone(), None));
+            let mut builder = WalletBuilder::new()
+                .mint_url(mint_url.clone())
+                .unit(unit.clone())
+                .localstore(self.localstore.clone())
+                .seed(self.seed)
+                .target_proof_count(target_proof_count)
+                .client(client);
+
+            if let Some(ttl) = metadata_cache_ttl {
+                builder = builder.set_metadata_cache_ttl(Some(ttl));
+            }
+
+            builder.build()?
+        } else {
+            #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
+            if let Some(tor) = &self.shared_tor_transport {
+                // Create wallet with Tor transport client, cloning the shared transport
+                let client = crate::wallet::TorHttpClient::with_transport(
+                    mint_url.clone(),
+                    tor.clone(),
+                    None,
+                );
+
+                let mut builder = WalletBuilder::new()
+                    .mint_url(mint_url.clone())
+                    .unit(unit.clone())
+                    .localstore(self.localstore.clone())
+                    .seed(self.seed)
+                    .target_proof_count(target_proof_count)
+                    .client(client);
+
+                if let Some(ttl) = metadata_cache_ttl {
+                    builder = builder.set_metadata_cache_ttl(Some(ttl));
+                }
+
+                builder.build()?
+            } else {
+                // Create wallet with default client
+                let wallet = Wallet::new(
+                    &mint_url.to_string(),
+                    unit.clone(),
+                    self.localstore.clone(),
+                    self.seed,
+                    Some(target_proof_count),
+                )?;
+                if let Some(ttl) = metadata_cache_ttl {
+                    wallet.set_metadata_cache_ttl(Some(ttl));
+                }
+                wallet
+            }
+
+            #[cfg(not(all(feature = "tor", not(target_arch = "wasm32"))))]
+            {
+                // Create wallet with default client
+                let wallet = Wallet::new(
+                    &mint_url.to_string(),
+                    unit.clone(),
+                    self.localstore.clone(),
+                    self.seed,
+                    Some(target_proof_count),
+                )?;
+                if let Some(ttl) = metadata_cache_ttl {
+                    wallet.set_metadata_cache_ttl(Some(ttl));
+                }
+                wallet
+            }
+        };
+
+        Ok(wallet)
+    }
+
+    /// Load all wallets from database
+    ///
+    /// This loads wallets for all mints stored in the database.
+    /// For each mint, it fetches the mint info to discover supported units
+    /// and creates a wallet for each supported unit.
+    #[instrument(skip(self))]
+    async fn load_wallets(&self) -> Result<(), Error> {
+        let mints = self.localstore.get_mints().await.map_err(Error::Database)?;
+
+        for (mint_url, _mint_info) in mints {
+            // Try to fetch mint info and create wallets for all supported units
+            // If fetch fails, fall back to creating just a Sat wallet
+            let units = match self.fetch_mint_info(&mint_url).await {
+                Ok(info) => {
+                    let supported = info.supported_units();
+                    if supported.is_empty() {
+                        vec![CurrencyUnit::Sat]
+                    } else {
+                        supported.into_iter().cloned().collect()
+                    }
+                }
+                Err(_) => {
+                    // If we can't fetch mint info, use default Sat unit for backward compatibility
+                    vec![CurrencyUnit::Sat]
+                }
+            };
+
+            for unit in units {
+                let key = WalletKey::new(mint_url.clone(), unit.clone());
+                // Skip if wallet already exists
+                if self.wallets.read().await.contains_key(&key) {
+                    continue;
+                }
+
+                let wallet = self
+                    .create_wallet_internal(mint_url.clone(), unit, None)
+                    .await?;
+
+                let mut wallets = self.wallets.write().await;
+                wallets.insert(key, wallet);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Get the currently active NpubCash mint URL
+    ///
+    /// Returns the mint URL that has been set as active for NpubCash operations,
+    /// or None if no active mint has been configured.
+    #[cfg(feature = "npubcash")]
+    pub async fn get_active_npubcash_mint(&self) -> Result<Option<MintUrl>, Error> {
+        use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
+        let value = self
+            .localstore
+            .kv_read(NPUBCASH_KV_NAMESPACE, "", ACTIVE_MINT_KEY)
+            .await?;
+        match value {
+            Some(bytes) => {
+                let s = String::from_utf8(bytes)
+                    .map_err(|_| Error::Custom("Invalid active mint URL".into()))?;
+                Ok(Some(MintUrl::from_str(&s)?))
+            }
+            None => Ok(None),
+        }
+    }
+
+    /// Set the active NpubCash mint URL
+    ///
+    /// This sets the mint that will be used for NpubCash operations.
+    #[cfg(feature = "npubcash")]
+    pub async fn set_active_npubcash_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
+        use super::npubcash::{ACTIVE_MINT_KEY, NPUBCASH_KV_NAMESPACE};
+        self.localstore
+            .kv_write(
+                NPUBCASH_KV_NAMESPACE,
+                "",
+                ACTIVE_MINT_KEY,
+                mint_url.to_string().as_bytes(),
+            )
+            .await?;
+        Ok(())
+    }
+
+    /// Sync NpubCash quotes from the active mint
+    ///
+    /// Retrieves pending mint quotes from the currently active NpubCash mint.
+    /// Returns an error if no active mint has been configured.
+    /// Uses Sat as the default unit for NpubCash operations.
+    #[cfg(feature = "npubcash")]
+    pub async fn sync_npubcash_quotes(
+        &self,
+    ) -> Result<Vec<crate::wallet::types::MintQuote>, Error> {
+        let active_mint = self.get_active_npubcash_mint().await?;
+        if let Some(mint_url) = active_mint {
+            // NpubCash typically uses Sat, try to find a Sat wallet first
+            let wallet = self.get_wallet(&mint_url, &CurrencyUnit::Sat).await?;
+            wallet.sync_npubcash_quotes().await
+        } else {
+            Err(Error::Custom("No active NpubCash mint set".into()))
+        }
+    }
+
+    // =========================================================================
+    // Helper functions for token and proof operations
+    // =========================================================================
+
+    /// Get token data (mint URL and proofs) from a token
+    ///
+    /// This method extracts the mint URL and proofs from a token. It will automatically
+    /// fetch the keysets from the mint if needed to properly decode the proofs.
+    ///
+    /// The mint must already be added to the wallet. If the mint is not in the wallet,
+    /// use `add_mint` first or set `allow_untrusted` in receive options.
+    ///
+    /// # Arguments
+    ///
+    /// * `token` - The token to extract data from
+    ///
+    /// # Returns
+    ///
+    /// A `TokenData` struct containing the mint URL and proofs
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use cdk::wallet::WalletRepository;
+    /// # use cdk::nuts::Token;
+    /// # use std::str::FromStr;
+    /// # async fn example(wallet: &WalletRepository) -> Result<(), Box<dyn std::error::Error>> {
+    /// let token = Token::from_str("cashuA...")?;
+    /// let token_data = wallet.get_token_data(&token).await?;
+    /// println!("Mint: {}", token_data.mint_url);
+    /// println!("Proofs: {} total", token_data.proofs.len());
+    /// # Ok(())
+    /// # }
+    /// ```
+    #[instrument(skip(self, token))]
+    pub async fn get_token_data(
+        &self,
+        token: &crate::nuts::nut00::Token,
+    ) -> Result<TokenData, Error> {
+        let mint_url = token.mint_url()?;
+        let unit = token.unit().unwrap_or_default();
+
+        // Get the keysets for this mint using the token's unit
+        let wallet = self.get_wallet(&mint_url, &unit).await?;
+        let keysets = wallet.get_mint_keysets().await?;
+        // Extract proofs using the keysets
+        let proofs = token.proofs(&keysets)?;
+
+        // Get the memo
+        let memo = token.memo().clone();
+        let redeem_fee = wallet.get_proofs_fee(&proofs).await?;
+
+        Ok(TokenData {
+            value: cdk_common::nuts::nut00::ProofsMethods::total_amount(&proofs)?,
+            mint_url,
+            proofs,
+            memo,
+            unit,
+            redeem_fee: Some(redeem_fee.total),
+        })
+    }
+
+    /// List proofs for all wallets
+    ///
+    /// Returns a map of (mint URL, currency unit) to proofs for each wallet in the repository.
+    #[instrument(skip(self))]
+    pub async fn list_proofs(
+        &self,
+    ) -> Result<std::collections::BTreeMap<WalletKey, Vec<cdk_common::Proof>>, Error> {
+        let mut mint_proofs = std::collections::BTreeMap::new();
+
+        for (key, wallet) in self.wallets.read().await.iter() {
+            let wallet_proofs = wallet.get_unspent_proofs().await?;
+            mint_proofs.insert(key.clone(), wallet_proofs);
+        }
+        Ok(mint_proofs)
+    }
+
+    /// List transactions across all wallets
+    #[instrument(skip(self))]
+    pub async fn list_transactions(
+        &self,
+        direction: Option<cdk_common::wallet::TransactionDirection>,
+    ) -> Result<Vec<cdk_common::wallet::Transaction>, Error> {
+        let mut transactions = Vec::new();
+
+        for (_, wallet) in self.wallets.read().await.iter() {
+            let wallet_transactions = wallet.list_transactions(direction).await?;
+            transactions.extend(wallet_transactions);
+        }
+
+        transactions.sort();
+
+        Ok(transactions)
+    }
+
+    /// Check all pending mint quotes and mint any that are paid
+    #[instrument(skip(self))]
+    pub async fn check_all_mint_quotes(
+        &self,
+        mint_url: Option<MintUrl>,
+    ) -> Result<cdk_common::Amount, Error> {
+        let mut total_minted = cdk_common::Amount::ZERO;
+
+        let wallets = self.wallets.read().await;
+        let wallets_to_check: Vec<_> = match &mint_url {
+            Some(url) => {
+                // Get all wallets for this mint (any currency unit)
+                let filtered: Vec<_> = wallets
+                    .iter()
+                    .filter(|(key, _)| &key.mint_url == url)
+                    .map(|(_, wallet)| wallet.clone())
+                    .collect();
+
+                if filtered.is_empty() {
+                    return Err(Error::UnknownMint {
+                        mint_url: url.to_string(),
+                    });
+                }
+                filtered
+            }
+            None => wallets.values().cloned().collect(),
+        };
+        drop(wallets);
+
+        for wallet in wallets_to_check {
+            let minted = wallet.mint_unissued_quotes().await?;
+            total_minted += minted;
+        }
+
+        Ok(total_minted)
+    }
+}
+
+impl Drop for WalletRepository {
+    fn drop(&mut self) {
+        self.seed.zeroize();
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use cdk_common::database::WalletDatabase;
+
+    use super::*;
+
+    async fn create_test_repository() -> WalletRepository {
+        let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
+            cdk_sqlite::wallet::memory::empty()
+                .await
+                .expect("Failed to create in-memory database"),
+        );
+        let seed = [0u8; 64];
+        WalletRepositoryBuilder::new()
+            .localstore(localstore)
+            .seed(seed)
+            .build()
+            .await
+            .expect("Failed to create WalletRepository")
+    }
+
+    #[tokio::test]
+    async fn test_wallet_repository_creation() {
+        let repo = create_test_repository().await;
+        assert!(repo.wallets.try_read().is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_has_mint_empty() {
+        let repo = create_test_repository().await;
+        let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
+        assert!(!repo.has_mint(&mint_url).await);
+    }
+
+    #[tokio::test]
+    async fn test_create_and_get_wallet() {
+        let repo = create_test_repository().await;
+        let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
+
+        // Create a wallet
+        let wallet = repo
+            .create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
+            .await
+            .expect("Failed to create wallet");
+
+        assert_eq!(wallet.mint_url, mint_url);
+        assert_eq!(wallet.unit, CurrencyUnit::Sat);
+
+        // Verify we can get it back
+        assert!(repo.has_mint(&mint_url).await);
+        assert!(repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
+        let retrieved = repo.get_wallet(&mint_url, &CurrencyUnit::Sat).await;
+        assert!(retrieved.is_ok());
+    }
+
+    #[tokio::test]
+    async fn test_remove_wallet() {
+        let repo = create_test_repository().await;
+        let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
+
+        // Create and then remove
+        repo.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
+            .await
+            .expect("Failed to create wallet");
+
+        assert!(repo.has_mint(&mint_url).await);
+        assert!(repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
+        let _ = repo
+            .remove_wallet(mint_url.clone(), CurrencyUnit::Sat)
+            .await;
+        assert!(!repo.has_mint(&mint_url).await);
+        assert!(!repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
+    }
+
+    #[tokio::test]
+    async fn test_get_wallets() {
+        let repo = create_test_repository().await;
+
+        let mint1: MintUrl = "https://mint1.example.com".parse().unwrap();
+        let mint2: MintUrl = "https://mint2.example.com".parse().unwrap();
+
+        repo.create_wallet(mint1, CurrencyUnit::Sat, None)
+            .await
+            .expect("Failed to create wallet 1");
+        repo.create_wallet(mint2, CurrencyUnit::Sat, None)
+            .await
+            .expect("Failed to create wallet 2");
+
+        let wallets = repo.get_wallets().await;
+        assert_eq!(wallets.len(), 2);
+    }
+
+    #[tokio::test]
+    async fn test_remove_wallet_does_not_touch_db() {
+        let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
+            cdk_sqlite::wallet::memory::empty()
+                .await
+                .expect("Failed to create in-memory database"),
+        );
+        let seed = [0u8; 64];
+        let repo = WalletRepositoryBuilder::new()
+            .localstore(localstore.clone())
+            .seed(seed)
+            .build()
+            .await
+            .expect("Failed to create WalletRepository");
+
+        let mint_url: MintUrl = "https://mint.example.com".parse().unwrap();
+
+        // Add mint to DB manually to simulate existing state
+        localstore.add_mint(mint_url.clone(), None).await.unwrap();
+
+        // Create wallet in repo
+        repo.create_wallet(mint_url.clone(), CurrencyUnit::Sat, None)
+            .await
+            .expect("Failed to create wallet");
+
+        // Remove wallet from in-memory repo
+        repo.remove_wallet(mint_url.clone(), CurrencyUnit::Sat)
+            .await
+            .expect("Failed to remove wallet");
+
+        // Verify wallet is gone from in-memory repo
+        assert!(!repo.has_wallet(&mint_url, &CurrencyUnit::Sat).await);
+
+        // Verify mint is still in DB (remove_wallet does not touch DB)
+        assert!(localstore
+            .get_mint(mint_url.clone())
+            .await
+            .unwrap()
+            .is_some());
+    }
+}

+ 3 - 3
misc/fake_itests.sh

@@ -190,13 +190,13 @@ if [ $status3 -ne 0 ]; then
 fi
 
 # Run fourth test (multi_mint_wallet) only if previous tests succeeded
-echo "Running multi_mint_wallet test"
-cargo test -p cdk-integration-tests --test multi_mint_wallet -- --nocapture
+echo "Running wallet_repository test"
+cargo test -p cdk-integration-tests --test wallet_repository -- --nocapture
 status4=$?
 
 # Exit with the status of the fourth test
 if [ $status4 -ne 0 ]; then
-    echo "Fourth test (multi_mint_wallet) failed with status $status4, exiting"
+    echo "Fourth test (wallet_repository) failed with status $status4, exiting"
     exit $status4
 fi