Parcourir la source

MultiMintWallet Refactor (#1001)

David Caseria il y a 1 mois
Parent
commit
23cba67c3b

+ 25 - 54
crates/cdk-cli/src/main.rs

@@ -9,7 +9,7 @@ use bip39::Mnemonic;
 use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::{HttpClient, MultiMintWallet, Wallet, WalletBuilder};
+use cdk::wallet::MultiMintWallet;
 #[cfg(feature = "redb")]
 use cdk_redb::WalletRedbDatabase;
 use cdk_sqlite::WalletSqliteDatabase;
@@ -49,6 +49,9 @@ struct Cli {
     /// NWS Proxy
     #[arg(short, long)]
     proxy: Option<Url>,
+    /// Currency unit to use for the wallet
+    #[arg(short, long, default_value = "sat")]
+    unit: String,
     #[command(subcommand)]
     command: Commands,
 }
@@ -67,6 +70,8 @@ enum Commands {
     Receive(sub_commands::receive::ReceiveSubCommand),
     /// Send
     Send(sub_commands::send::SendSubCommand),
+    /// Transfer tokens between mints
+    Transfer(sub_commands::transfer::TransferSubCommand),
     /// Reclaim pending proofs that are no longer pending
     CheckPending,
     /// View mint info
@@ -168,62 +173,25 @@ async fn main() -> Result<()> {
     };
     let seed = mnemonic.to_seed_normalized("");
 
-    let mut wallets: Vec<Wallet> = Vec::new();
+    // Parse currency unit from args
+    let currency_unit = CurrencyUnit::from_str(&args.unit)
+        .unwrap_or_else(|_| CurrencyUnit::Custom(args.unit.clone()));
 
-    let mints = localstore.get_mints().await?;
-
-    for (mint_url, mint_info) in mints {
-        let units = if let Some(mint_info) = mint_info {
-            mint_info.supported_units().into_iter().cloned().collect()
-        } else {
-            vec![CurrencyUnit::Sat]
-        };
-
-        let proxy_client = if let Some(proxy_url) = args.proxy.as_ref() {
-            Some(HttpClient::with_proxy(
-                mint_url.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) => {
+            // Create MultiMintWallet with proxy configuration
+            MultiMintWallet::new_with_proxy(
+                localstore.clone(),
+                seed,
+                currency_unit.clone(),
                 proxy_url.clone(),
-                None,
-                true,
-            )?)
-        } else {
-            None
-        };
-
-        let seed = mnemonic.to_seed_normalized("");
-
-        for unit in units {
-            let mint_url_clone = mint_url.clone();
-            let mut builder = WalletBuilder::new()
-                .mint_url(mint_url_clone.clone())
-                .unit(unit)
-                .localstore(localstore.clone())
-                .seed(seed);
-
-            if let Some(http_client) = &proxy_client {
-                builder = builder.client(http_client.clone());
-            }
-
-            let wallet = builder.build()?;
-
-            let wallet_clone = wallet.clone();
-
-            tokio::spawn(async move {
-                // We refresh keysets, this internally gets mint info
-                if let Err(err) = wallet_clone.refresh_keysets().await {
-                    tracing::error!(
-                        "Could not get mint quote for {}, {}",
-                        wallet_clone.mint_url,
-                        err
-                    );
-                }
-            });
-
-            wallets.push(wallet);
+            )
+            .await?
         }
-    }
-
-    let multi_mint_wallet = MultiMintWallet::new(localstore, seed, wallets);
+        None => MultiMintWallet::new(localstore.clone(), seed, currency_unit.clone()).await?,
+    };
 
     match &args.command {
         Commands::DecodeToken(sub_command_args) => {
@@ -239,6 +207,9 @@ async fn main() -> Result<()> {
         Commands::Send(sub_command_args) => {
             sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
         }
+        Commands::Transfer(sub_command_args) => {
+            sub_commands::transfer::transfer(&multi_mint_wallet, sub_command_args).await
+        }
         Commands::CheckPending => {
             sub_commands::check_pending::check_pending(&multi_mint_wallet).await
         }

+ 16 - 3
crates/cdk-cli/src/sub_commands/balance.rs

@@ -3,11 +3,24 @@ use std::collections::BTreeMap;
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::wallet::MultiMintWallet;
 use cdk::Amount;
 
 pub async fn balance(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
-    mint_balances(multi_mint_wallet, &CurrencyUnit::Sat).await?;
+    // Show individual mint balances
+    let mint_balances = mint_balances(multi_mint_wallet, multi_mint_wallet.unit()).await?;
+
+    // Show total balance using the new unified interface
+    let total = multi_mint_wallet.total_balance().await?;
+    if !mint_balances.is_empty() {
+        println!();
+        println!(
+            "Total balance across all wallets: {} {}",
+            total,
+            multi_mint_wallet.unit()
+        );
+    }
+
     Ok(())
 }
 
@@ -15,7 +28,7 @@ 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(unit).await?;
+    let wallets: BTreeMap<MintUrl, Amount> = multi_mint_wallet.get_balances().await?;
 
     let mut wallets_vec = Vec::with_capacity(wallets.len());
 

+ 1 - 12
crates/cdk-cli/src/sub_commands/burn.rs

@@ -1,9 +1,5 @@
-use std::str::FromStr;
-
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::nuts::CurrencyUnit;
-use cdk::wallet::types::WalletKey;
 use cdk::wallet::MultiMintWallet;
 use cdk::Amount;
 use clap::Args;
@@ -12,9 +8,6 @@ use clap::Args;
 pub struct BurnSubCommand {
     /// Mint Url
     mint_url: Option<MintUrl>,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: String,
 }
 
 pub async fn burn(
@@ -22,14 +15,10 @@ pub async fn burn(
     sub_command_args: &BurnSubCommand,
 ) -> Result<()> {
     let mut total_burnt = Amount::ZERO;
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
 
     match &sub_command_args.mint_url {
         Some(mint_url) => {
-            let wallet = multi_mint_wallet
-                .get_wallet(&WalletKey::new(mint_url.clone(), unit))
-                .await
-                .unwrap();
+            let wallet = multi_mint_wallet.get_wallet(mint_url).await.unwrap();
             total_burnt = wallet.check_all_pending_proofs().await?;
         }
         None => {

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

@@ -1,11 +1,9 @@
 use std::path::Path;
-use std::str::FromStr;
 use std::time::Duration;
 
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::{CurrencyUnit, MintInfo};
-use cdk::wallet::types::WalletKey;
+use cdk::nuts::MintInfo;
 use cdk::wallet::MultiMintWallet;
 use cdk::OidcClient;
 use clap::Args;
@@ -18,10 +16,6 @@ use crate::token_storage;
 pub struct CatDeviceLoginSubCommand {
     /// Mint url
     mint_url: MintUrl,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    #[arg(short, long)]
-    unit: String,
     /// Client ID for OIDC authentication
     #[arg(default_value = "cashu-client")]
     #[arg(long)]
@@ -34,17 +28,15 @@ pub async fn cat_device_login(
     work_dir: &Path,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
 
-    let wallet = match multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-    {
+    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
         Some(wallet) => wallet.clone(),
         None => {
+            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
             multi_mint_wallet
-                .create_and_add_wallet(&mint_url.to_string(), unit, None)
-                .await?
+                .get_wallet(&mint_url)
+                .await
+                .expect("Wallet should exist after adding mint")
         }
     };
 

+ 7 - 14
crates/cdk-cli/src/sub_commands/cat_login.rs

@@ -1,10 +1,8 @@
 use std::path::Path;
-use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::{CurrencyUnit, MintInfo};
-use cdk::wallet::types::WalletKey;
+use cdk::nuts::MintInfo;
 use cdk::wallet::MultiMintWallet;
 use cdk::OidcClient;
 use clap::Args;
@@ -20,10 +18,6 @@ pub struct CatLoginSubCommand {
     username: String,
     /// Password
     password: String,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    #[arg(short, long)]
-    unit: String,
     /// Client ID for OIDC authentication
     #[arg(default_value = "cashu-client")]
     #[arg(long)]
@@ -36,17 +30,16 @@ pub async fn cat_login(
     work_dir: &Path,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
 
-    let wallet = match multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-    {
+    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
         Some(wallet) => wallet.clone(),
         None => {
+            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
             multi_mint_wallet
-                .create_and_add_wallet(&mint_url.to_string(), unit, None)
-                .await?
+                .get_wallet(&mint_url)
+                .await
+                .expect("Wallet should exist after adding mint")
+                .clone()
         }
     };
 

+ 1 - 4
crates/cdk-cli/src/sub_commands/create_request.rs

@@ -6,9 +6,6 @@ use clap::Args;
 pub struct CreateRequestSubCommand {
     #[arg(short, long)]
     amount: Option<u64>,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: String,
     /// Quote description
     description: Option<String>,
     /// P2PK: Public key(s) for which the token can be spent with valid signature(s)
@@ -48,7 +45,7 @@ pub async fn create_request(
     // Gather parameters for library call
     let params = pr::CreateRequestParams {
         amount: sub_command_args.amount,
-        unit: sub_command_args.unit.clone(),
+        unit: multi_mint_wallet.unit().to_string(),
         description: sub_command_args.description.clone(),
         pubkeys: sub_command_args.pubkey.clone(),
         num_sigs: sub_command_args.num_sigs,

+ 243 - 230
crates/cdk-cli/src/sub_commands/melt.rs

@@ -4,19 +4,12 @@ use anyhow::{bail, Result};
 use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::{CurrencyUnit, MeltOptions};
-use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::types::WalletKey;
-use cdk::wallet::{MeltQuote, Wallet};
+use cdk::wallet::MultiMintWallet;
 use cdk::Bolt11Invoice;
 use clap::{Args, ValueEnum};
 use lightning::offers::offer::Offer;
-use tokio::task::JoinSet;
 
-use crate::sub_commands::balance::mint_balances;
-use crate::utils::{
-    get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
-    validate_mint_number,
-};
+use crate::utils::{get_number_input, get_user_input};
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
 pub enum PaymentType {
@@ -30,40 +23,17 @@ pub enum PaymentType {
 
 #[derive(Args)]
 pub struct MeltSubCommand {
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: String,
     /// Mpp
     #[arg(short, long, conflicts_with = "mint_url")]
     mpp: bool,
     /// Mint URL to use for melting
     #[arg(long, conflicts_with = "mpp")]
     mint_url: Option<String>,
-    /// Payment method (bolt11 or bolt12)
+    /// Payment method (bolt11, bolt12, or bip353)
     #[arg(long, default_value = "bolt11")]
     method: PaymentType,
 }
 
-/// Helper function to process a melt quote and execute the payment
-async fn process_payment(wallet: &Wallet, quote: MeltQuote) -> Result<()> {
-    // Display quote information
-    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 payment
-    let melt = wallet.melt(&quote.id).await?;
-    println!("Paid: {}", melt.state);
-
-    if let Some(preimage) = melt.preimage {
-        println!("Payment preimage: {preimage}");
-    }
-
-    Ok(())
-}
-
 /// Helper function to check if there are enough funds and create appropriate MeltOptions
 fn create_melt_options(
     available_funds: u64,
@@ -95,71 +65,149 @@ pub async fn pay(
     multi_mint_wallet: &MultiMintWallet,
     sub_command_args: &MeltSubCommand,
 ) -> Result<()> {
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
-    let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
+    // Check total balance across all wallets
+    let total_balance = multi_mint_wallet.total_balance().await?;
+    if total_balance == Amount::ZERO {
+        bail!("No funds available");
+    }
 
     if sub_command_args.mpp {
-        // MPP logic only works with BOLT11 currently
+        // 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");
         }
 
-        // Collect mint numbers and amounts for MPP
-        let (mints, mint_amounts) = collect_mpp_inputs(&mints_amounts, &sub_command_args.mint_url)?;
-
-        // Process BOLT11 MPP payment
-        let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
-
-        // Get quotes from all mints
-        let quotes = get_mpp_quotes(
-            multi_mint_wallet,
-            &mints_amounts,
-            &mints,
-            &mint_amounts,
-            &unit,
-            &bolt11,
-        )
-        .await?;
-
-        // Execute all melts
-        execute_mpp_melts(quotes).await?;
-    } else {
-        // Get wallet either by mint URL or by index
-        let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
-            // Use the provided mint URL
-            get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await?
-        } else {
-            // Fallback to the index-based selection
-            let mint_number: usize = get_number_input("Enter mint number to melt from")?;
-            get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
-                .await?
-        };
-
-        // Find the mint amount for the selected wallet to check available funds
-        let mint_url = &wallet.mint_url;
-        let mint_amount = mints_amounts
+        let bolt11_str = get_user_input("Enter bolt11 invoice")?;
+        let _bolt11 = Bolt11Invoice::from_str(&bolt11_str)?; // Validate invoice format
+
+        // 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()
+            );
+        }
+
+        // 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)")?;
+
+            if mint_input.to_lowercase() == "done" || mint_input.is_empty() {
+                break;
+            }
+
+            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");
+        }
+
+        // Get quotes for each mint
+        println!("\nGetting melt quotes...");
+        let quotes = multi_mint_wallet
+            .mpp_melt_quote(bolt11_str, mint_amounts)
+            .await?;
+
+        // 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);
+        }
+
+        // Execute the melts
+        let quotes_to_execute: Vec<(MintUrl, String)> = quotes
             .iter()
-            .find(|(url, _)| url == mint_url)
-            .map(|(_, amount)| *amount)
-            .ok_or_else(|| anyhow::anyhow!("Could not find balance for mint: {}", mint_url))?;
+            .map(|(url, quote)| (url.clone(), quote.id.clone()))
+            .collect();
 
-        let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
+        println!("\nExecuting MPP payment...");
+        let results = multi_mint_wallet.mpp_melt(quotes_to_execute).await?;
 
-        // Process payment based on payment method
+        // Display results
+        println!("\nPayment results:");
+        let mut total_paid = Amount::ZERO;
+        let mut total_fees = Amount::ZERO;
+
+        for (mint_url, melted) in results {
+            println!(
+                "  {} - Paid: {}, Fee: {}",
+                mint_url, melted.amount, melted.fee_paid
+            );
+            total_paid += melted.amount;
+            total_fees += melted.fee_paid;
+
+            if let Some(preimage) = melted.preimage {
+                println!("    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 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice")?)?;
+                let bolt11_str = get_user_input("Enter bolt11 invoice")?;
+                let bolt11 = Bolt11Invoice::from_str(&bolt11_str)?;
 
                 // Determine payment amount and options
-                let prompt =
-                    "Enter the amount you would like to pay in sats for this amountless invoice.";
+                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)?;
+                    create_melt_options(available_funds, bolt11.amount_milli_satoshis(), &prompt)?;
+
+                // Use mint-specific functions or auto-select
+                let melted = if let Some(mint_url) = &sub_command_args.mint_url {
+                    // User specified a mint - use the new mint-specific functions
+                    let mint_url = MintUrl::from_str(mint_url)?;
+
+                    // Create a melt quote for the specific mint
+                    let quote = multi_mint_wallet
+                        .melt_quote(&mint_url, bolt11_str.clone(), options)
+                        .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 {
+                    // Let the wallet automatically select the best mint
+                    multi_mint_wallet.melt(&bolt11_str, options, None).await?
+                };
 
-                // Process payment
-                let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
-                process_payment(&wallet, quote).await?;
+                println!("Payment successful: {:?}", melted);
+                if let Some(preimage) = melted.preimage {
+                    println!("Payment preimage: {}", preimage);
+                }
             }
             PaymentType::Bolt12 => {
                 // Process BOLT12 payment (offer)
@@ -168,26 +216,117 @@ pub async fn pay(
                     .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
 
                 // Determine if offer has an amount
-                let prompt =
-                    "Enter the amount you would like to pay in sats for this amountless offer:";
+                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)?;
+                let options = create_melt_options(available_funds, amount_msat, &prompt)?;
+
+                // Get wallet for BOLT12
+                let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
+                    // User specified a mint
+                    let mint_url = MintUrl::from_str(mint_url)?;
+                    multi_mint_wallet
+                        .get_wallet(&mint_url)
+                        .await
+                        .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?
+                } else {
+                    // Show available mints and let user select
+                    let balances = multi_mint_wallet.get_balances().await?;
+                    println!("\nAvailable mints:");
+                    for (i, (mint_url, balance)) in balances.iter().enumerate() {
+                        println!(
+                            "  {}: {} - {} {}",
+                            i,
+                            mint_url,
+                            balance,
+                            multi_mint_wallet.unit()
+                        );
+                    }
+
+                    let mint_number: usize = get_number_input("Enter mint number to melt from")?;
+                    let selected_mint = balances
+                        .iter()
+                        .nth(mint_number)
+                        .map(|(url, _)| url)
+                        .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))?;
+
+                    multi_mint_wallet
+                        .get_wallet(selected_mint)
+                        .await
+                        .ok_or_else(|| anyhow::anyhow!("Mint {} not found", selected_mint))?
+                };
 
                 // Get melt quote for BOLT12
                 let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
-                process_payment(&wallet, quote).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.melt(&quote.id).await?;
+                println!(
+                    "Payment successful: Paid {} with fee {}",
+                    melted.amount, melted.fee_paid
+                );
+                if let Some(preimage) = melted.preimage {
+                    println!("Payment preimage: {}", preimage);
+                }
             }
             PaymentType::Bip353 => {
-                let bip353_addr = get_user_input("Enter Bip353 address.")?;
+                let bip353_addr = get_user_input("Enter Bip353 address")?;
 
-                let prompt =
-                    "Enter the amount you would like to pay in sats for this amountless offer:";
+                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)?;
+                let options = create_melt_options(available_funds, None, &prompt)?;
+
+                // Get wallet for BIP353
+                let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
+                    // User specified a mint
+                    let mint_url = MintUrl::from_str(mint_url)?;
+                    multi_mint_wallet
+                        .get_wallet(&mint_url)
+                        .await
+                        .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?
+                } else {
+                    // Show available mints and let user select
+                    let balances = multi_mint_wallet.get_balances().await?;
+                    println!("\nAvailable mints:");
+                    for (i, (mint_url, balance)) in balances.iter().enumerate() {
+                        println!(
+                            "  {}: {} - {} {}",
+                            i,
+                            mint_url,
+                            balance,
+                            multi_mint_wallet.unit()
+                        );
+                    }
+
+                    let mint_number: usize = get_number_input("Enter mint number to melt from")?;
+                    let selected_mint = balances
+                        .iter()
+                        .nth(mint_number)
+                        .map(|(url, _)| url)
+                        .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))?;
+
+                    multi_mint_wallet
+                        .get_wallet(selected_mint)
+                        .await
+                        .ok_or_else(|| anyhow::anyhow!("Mint {} not found", selected_mint))?
+                };
 
                 // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
                 let quote = wallet
@@ -196,153 +335,27 @@ pub async fn pay(
                         options.expect("Amount is required").amount_msat(),
                     )
                     .await?;
-                process_payment(&wallet, quote).await?;
-            }
-        }
-    }
-
-    Ok(())
-}
-
-/// Collect mint numbers and amounts for MPP payments
-fn collect_mpp_inputs(
-    mints_amounts: &[(MintUrl, Amount)],
-    mint_url_opt: &Option<String>,
-) -> Result<(Vec<usize>, Vec<u64>)> {
-    let mut mints = Vec::new();
-    let mut mint_amounts = Vec::new();
-
-    // If a specific mint URL was provided, try to use it as the first mint
-    if let Some(mint_url) = mint_url_opt {
-        println!("Using mint URL {mint_url} as the first mint for MPP payment.");
-
-        // Find the index of this mint in the mints_amounts list
-        if let Some(mint_index) = mints_amounts
-            .iter()
-            .position(|(url, _)| url.to_string() == *mint_url)
-        {
-            mints.push(mint_index);
-            let melt_amount: u64 =
-                get_number_input("Enter amount to mint from this mint in sats.")?;
-            mint_amounts.push(melt_amount);
-        } else {
-            println!(
-                "Warning: Mint URL not found or no balance. Continuing with manual selection."
-            );
-        }
-    }
-
-    // Continue with regular mint selection
-    loop {
-        let mint_number: String =
-            get_user_input("Enter mint number to melt from and -1 when done.")?;
-
-        if mint_number == "-1" || mint_number.is_empty() {
-            break;
-        }
-
-        let mint_number: usize = mint_number.parse()?;
-        validate_mint_number(mint_number, mints_amounts.len())?;
-
-        mints.push(mint_number);
-        let melt_amount: u64 = get_number_input("Enter amount to mint from this mint in sats.")?;
-        mint_amounts.push(melt_amount);
-    }
-
-    if mints.is_empty() {
-        bail!("No mints selected for MPP payment");
-    }
-
-    Ok((mints, mint_amounts))
-}
-
-/// Get quotes from all mints for MPP payment
-async fn get_mpp_quotes(
-    multi_mint_wallet: &MultiMintWallet,
-    mints_amounts: &[(MintUrl, Amount)],
-    mints: &[usize],
-    mint_amounts: &[u64],
-    unit: &CurrencyUnit,
-    bolt11: &Bolt11Invoice,
-) -> Result<Vec<(Wallet, MeltQuote)>> {
-    let mut quotes = JoinSet::new();
-
-    for (mint, amount) in mints.iter().zip(mint_amounts) {
-        let wallet = mints_amounts[*mint].0.clone();
-
-        let wallet = multi_mint_wallet
-            .get_wallet(&WalletKey::new(wallet, unit.clone()))
-            .await
-            .expect("Known wallet");
-        let options = MeltOptions::new_mpp(*amount * 1000);
-
-        let bolt11_clone = bolt11.clone();
-
-        quotes.spawn(async move {
-            let quote = wallet
-                .melt_quote(bolt11_clone.to_string(), Some(options))
-                .await;
-
-            (wallet, quote)
-        });
-    }
 
-    let quotes_results = quotes.join_all().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);
 
-    // Validate all quotes succeeded
-    let mut valid_quotes = Vec::new();
-    for (wallet, quote_result) in quotes_results {
-        match quote_result {
-            Ok(quote) => {
+                // Execute the melt
+                let melted = wallet.melt(&quote.id).await?;
                 println!(
-                    "Melt quote {} for mint {} of amount {} with fee {}.",
-                    quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
+                    "Payment successful: Paid {} with fee {}",
+                    melted.amount, melted.fee_paid
                 );
-                valid_quotes.push((wallet, quote));
-            }
-            Err(err) => {
-                tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, err);
-                bail!("Could not get melt quote for {}", wallet.mint_url);
+                if let Some(preimage) = melted.preimage {
+                    println!("Payment preimage: {}", preimage);
+                }
             }
         }
     }
 
-    Ok(valid_quotes)
-}
-
-/// Execute all melts for MPP payment
-async fn execute_mpp_melts(quotes: Vec<(Wallet, MeltQuote)>) -> Result<()> {
-    let mut melts = JoinSet::new();
-
-    for (wallet, quote) in quotes {
-        melts.spawn(async move {
-            let melt = wallet.melt(&quote.id).await;
-            (wallet, melt)
-        });
-    }
-
-    let melts = melts.join_all().await;
-
-    let mut error = false;
-
-    for (wallet, melt) in melts {
-        match melt {
-            Ok(melt) => {
-                println!(
-                    "Melt for {} paid {} with fee of {} ",
-                    wallet.mint_url, melt.amount, melt.fee_paid
-                );
-            }
-            Err(err) => {
-                println!("Melt for {} failed with {}", wallet.mint_url, err);
-                error = true;
-            }
-        }
-    }
-
-    if error {
-        bail!("Could not complete all melts");
-    }
-
     Ok(())
 }

+ 2 - 6
crates/cdk-cli/src/sub_commands/mint.rs

@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::nuts::PaymentMethod;
 use cdk::wallet::MultiMintWallet;
 use cdk::{Amount, StreamExt};
 use clap::Args;
@@ -18,9 +18,6 @@ pub struct MintSubCommand {
     mint_url: MintUrl,
     /// Amount
     amount: Option<u64>,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: String,
     /// Quote description
     #[serde(skip_serializing_if = "Option::is_none")]
     description: Option<String>,
@@ -46,10 +43,9 @@ pub async fn mint(
     sub_command_args: &MintSubCommand,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
     let description: Option<String> = sub_command_args.description.clone();
 
-    let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
+    let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url).await?;
 
     let payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
 

+ 7 - 14
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs

@@ -1,10 +1,8 @@
 use std::path::Path;
-use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::{CurrencyUnit, MintInfo};
-use cdk::wallet::types::WalletKey;
+use cdk::nuts::MintInfo;
 use cdk::wallet::MultiMintWallet;
 use cdk::{Amount, OidcClient};
 use clap::Args;
@@ -21,10 +19,6 @@ pub struct MintBlindAuthSubCommand {
     /// Cat (access token)
     #[arg(long)]
     cat: Option<String>,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    #[arg(short, long)]
-    unit: String,
 }
 
 pub async fn mint_blind_auth(
@@ -33,17 +27,16 @@ pub async fn mint_blind_auth(
     work_dir: &Path,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
 
-    let wallet = match multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-    {
+    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
         Some(wallet) => wallet.clone(),
         None => {
+            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
             multi_mint_wallet
-                .create_and_add_wallet(&mint_url.to_string(), unit, None)
-                .await?
+                .get_wallet(&mint_url)
+                .await
+                .expect("Wallet should exist after adding mint")
+                .clone()
         }
     };
 

+ 1 - 0
crates/cdk-cli/src/sub_commands/mod.rs

@@ -16,4 +16,5 @@ pub mod pending_mints;
 pub mod receive;
 pub mod restore;
 pub mod send;
+pub mod transfer;
 pub mod update_mint_url;

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

@@ -2,11 +2,9 @@ use anyhow::Result;
 use cdk::wallet::MultiMintWallet;
 
 pub async fn mint_pending(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
-    let amounts = multi_mint_wallet.check_all_mint_quotes(None).await?;
+    let amount = multi_mint_wallet.check_all_mint_quotes(None).await?;
 
-    for (unit, amount) in amounts {
-        println!("Unit: {unit}, Amount: {amount}");
-    }
+    println!("Amount: {amount}");
 
     Ok(())
 }

+ 39 - 18
crates/cdk-cli/src/sub_commands/receive.rs

@@ -4,11 +4,11 @@ use std::str::FromStr;
 use std::time::Duration;
 
 use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
 use cdk::nuts::{SecretKey, Token};
 use cdk::util::unix_time;
 use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::types::WalletKey;
-use cdk::wallet::ReceiveOptions;
+use cdk::wallet::{MultiMintReceiveOptions, ReceiveOptions};
 use cdk::Amount;
 use clap::Args;
 use nostr_sdk::nips::nip04;
@@ -36,6 +36,12 @@ pub struct ReceiveSubCommand {
     /// Preimage
     #[arg(short, long,  action = clap::ArgAction::Append)]
     preimage: Vec<String>,
+    /// 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(
@@ -69,6 +75,8 @@ pub async fn receive(
                 token_str,
                 &signing_keys,
                 &sub_command_args.preimage,
+                sub_command_args.allow_untrusted,
+                sub_command_args.transfer_to.as_deref(),
             )
             .await?
         }
@@ -109,6 +117,8 @@ pub async fn receive(
                     token_str,
                     &signing_keys,
                     &sub_command_args.preimage,
+                    sub_command_args.allow_untrusted,
+                    sub_command_args.transfer_to.as_deref(),
                 )
                 .await
                 {
@@ -135,29 +145,40 @@ async fn receive_token(
     token_str: &str,
     signing_keys: &[SecretKey],
     preimage: &[String],
+    allow_untrusted: bool,
+    transfer_to: Option<&str>,
 ) -> Result<Amount> {
     let token: Token = Token::from_str(token_str)?;
 
     let mint_url = token.mint_url()?;
-    let unit = token.unit().unwrap_or_default();
-
-    if multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-        .is_none()
-    {
-        get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
+
+    // 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();
+
+    // If mint is not trusted and we don't allow untrusted, add it first (old behavior)
+    if !is_trusted && !allow_untrusted {
+        get_or_create_wallet(multi_mint_wallet, &mint_url).await?;
     }
 
+    // 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,
-            ReceiveOptions {
-                p2pk_signing_keys: signing_keys.to_vec(),
-                preimages: preimage.to_vec(),
-                ..Default::default()
-            },
-        )
+        .receive(token_str, multi_mint_options)
         .await?;
     Ok(amount)
 }

+ 6 - 14
crates/cdk-cli/src/sub_commands/restore.rs

@@ -1,9 +1,5 @@
-use std::str::FromStr;
-
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::nuts::CurrencyUnit;
-use cdk::wallet::types::WalletKey;
 use cdk::wallet::MultiMintWallet;
 use clap::Args;
 
@@ -11,27 +7,23 @@ use clap::Args;
 pub struct RestoreSubCommand {
     /// Mint Url
     mint_url: MintUrl,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: String,
 }
 
 pub async fn restore(
     multi_mint_wallet: &MultiMintWallet,
     sub_command_args: &RestoreSubCommand,
 ) -> Result<()> {
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
     let mint_url = sub_command_args.mint_url.clone();
 
-    let wallet = match multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-    {
+    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
         Some(wallet) => wallet.clone(),
         None => {
+            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
             multi_mint_wallet
-                .create_and_add_wallet(&mint_url.to_string(), unit, None)
-                .await?
+                .get_wallet(&mint_url)
+                .await
+                .expect("Wallet should exist after adding mint")
+                .clone()
         }
     };
 

+ 96 - 47
crates/cdk-cli/src/sub_commands/send.rs

@@ -1,16 +1,14 @@
 use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
-use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
+use cdk::mint_url::MintUrl;
+use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
 use cdk::wallet::types::SendKind;
 use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
 use cdk::Amount;
 use clap::Args;
 
-use crate::sub_commands::balance::mint_balances;
-use crate::utils::{
-    check_sufficient_funds, get_number_input, get_wallet_by_index, get_wallet_by_mint_url,
-};
+use crate::utils::get_number_input;
 
 #[derive(Args)]
 pub struct SendSubCommand {
@@ -50,39 +48,38 @@ pub struct SendSubCommand {
     /// Mint URL to use for sending
     #[arg(long)]
     mint_url: Option<String>,
-    /// Currency unit e.g. sat
-    #[arg(default_value = "sat")]
-    unit: 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 allowed for transfers (can be specified multiple times)
+    #[arg(long, action = clap::ArgAction::Append)]
+    allowed_mints: Vec<String>,
+    /// Specific mints to exclude from transfers (can be specified multiple times)
+    #[arg(long, action = clap::ArgAction::Append)]
+    excluded_mints: Vec<String>,
 }
 
 pub async fn send(
     multi_mint_wallet: &MultiMintWallet,
     sub_command_args: &SendSubCommand,
 ) -> Result<()> {
-    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
-    let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
-
-    // Get wallet either by mint URL or by index
-    let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
-        // Use the provided mint URL
-        get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit).await?
-    } else {
-        // Fallback to the index-based selection
-        let mint_number: usize = get_number_input("Enter mint number to create token")?;
-        get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await?
-    };
+    let token_amount = Amount::from(get_number_input::<u64>(&format!(
+        "Enter value of token in {}",
+        multi_mint_wallet.unit()
+    ))?);
 
-    let token_amount = Amount::from(get_number_input::<u64>("Enter value of token in sats")?);
-
-    // Find the mint amount for the selected wallet to check if we have sufficient funds
-    let mint_url = &wallet.mint_url;
-    let mint_amount = mints_amounts
-        .iter()
-        .find(|(url, _)| url == mint_url)
-        .map(|(_, amount)| *amount)
-        .ok_or_else(|| anyhow!("Could not find balance for mint: {}", mint_url))?;
-
-    check_sufficient_funds(mint_amount, token_amount)?;
+    // Check total balance across all wallets
+    let total_balance = multi_mint_wallet.total_balance().await?;
+    if total_balance < token_amount {
+        return Err(anyhow!(
+            "Insufficient funds. Total balance: {}, Required: {}",
+            total_balance,
+            token_amount
+        ));
+    }
 
     let conditions = match (&sub_command_args.preimage, &sub_command_args.hash) {
         (Some(_), Some(_)) => {
@@ -206,22 +203,74 @@ pub async fn send(
         (false, None) => SendKind::OnlineExact,
     };
 
-    let prepared_send = wallet
-        .prepare_send(
-            token_amount,
-            SendOptions {
-                memo: sub_command_args.memo.clone().map(|memo| SendMemo {
-                    memo,
-                    include_memo: true,
-                }),
-                send_kind,
-                include_fee: sub_command_args.include_fee,
-                conditions,
-                ..Default::default()
-            },
-        )
-        .await?;
-    let token = prepared_send.confirm(None).await?;
+    let send_options = SendOptions {
+        memo: sub_command_args.memo.clone().map(|memo| SendMemo {
+            memo,
+            include_memo: true,
+        }),
+        send_kind,
+        include_fee: sub_command_args.include_fee,
+        conditions,
+        ..Default::default()
+    };
+
+    // Parse allowed and excluded mints from CLI arguments
+    let allowed_mints: Result<Vec<MintUrl>, _> = sub_command_args
+        .allowed_mints
+        .iter()
+        .map(|url| MintUrl::from_str(url))
+        .collect();
+    let allowed_mints = allowed_mints?;
+
+    let excluded_mints: Result<Vec<MintUrl>, _> = sub_command_args
+        .excluded_mints
+        .iter()
+        .map(|url| MintUrl::from_str(url))
+        .collect();
+    let excluded_mints = excluded_mints?;
+
+    // Create MultiMintSendOptions from CLI arguments
+    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,
+        excluded_mints,
+        send_options: send_options.clone(),
+    };
+
+    // Use the new unified interface
+    let token = if let Some(mint_url) = &sub_command_args.mint_url {
+        // User specified a mint, use that specific wallet
+        let mint_url = cdk::mint_url::MintUrl::from_str(mint_url)?;
+        let prepared = multi_mint_wallet
+            .prepare_send(mint_url, token_amount, multi_mint_options)
+            .await?;
+
+        // Confirm the prepared send (single mint)
+        let memo = send_options.memo.clone();
+        prepared.confirm(memo).await?
+    } else {
+        // Let the wallet automatically select the best mint
+        // First, get balances to find a mint with sufficient funds
+        let balances = multi_mint_wallet.get_balances().await?;
+
+        // Find a mint with sufficient balance
+        let mint_url = balances
+            .into_iter()
+            .find(|(_, balance)| *balance >= token_amount)
+            .map(|(mint_url, _)| mint_url)
+            .ok_or_else(|| {
+                anyhow::anyhow!("No mint has sufficient balance for the requested amount")
+            })?;
+
+        let prepared = multi_mint_wallet
+            .prepare_send(mint_url, token_amount, multi_mint_options)
+            .await?;
+
+        // Confirm the prepared send (multi mint)
+        let memo = send_options.memo.clone();
+        prepared.confirm(memo).await?
+    };
 
     match sub_command_args.v3 {
         true => {

+ 209 - 0
crates/cdk-cli/src/sub_commands/transfer.rs

@@ -0,0 +1,209 @@
+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::Amount;
+use clap::Args;
+
+use crate::utils::get_number_input;
+
+#[derive(Args)]
+pub struct TransferSubCommand {
+    /// Source mint URL to transfer from (optional - will prompt if not provided)
+    #[arg(long)]
+    source_mint: Option<String>,
+    /// Target mint URL to transfer to (optional - will prompt if not provided)
+    #[arg(long)]
+    target_mint: Option<String>,
+    /// Amount to transfer (optional - will prompt if not provided)
+    #[arg(short, long, conflicts_with = "full_balance")]
+    amount: Option<u64>,
+    /// Transfer all available balance from source mint
+    #[arg(long, conflicts_with = "amount")]
+    full_balance: bool,
+}
+
+/// Helper function to select a mint from available mints
+async fn select_mint(
+    multi_mint_wallet: &MultiMintWallet,
+    prompt: &str,
+    exclude_mint: Option<&MintUrl>,
+) -> Result<MintUrl> {
+    let balances = multi_mint_wallet.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))
+        .collect();
+
+    if available_mints.is_empty() {
+        bail!("No available mints found");
+    }
+
+    println!("\nAvailable mints:");
+    for (i, (mint_url, balance)) in available_mints.iter().enumerate() {
+        println!(
+            "  {}: {} - {} {}",
+            i,
+            mint_url,
+            balance,
+            multi_mint_wallet.unit()
+        );
+    }
+
+    let mint_number: usize = get_number_input(prompt)?;
+    available_mints
+        .get(mint_number)
+        .map(|(url, _)| (*url).clone())
+        .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))
+}
+
+pub async fn transfer(
+    multi_mint_wallet: &MultiMintWallet,
+    sub_command_args: &TransferSubCommand,
+) -> Result<()> {
+    // Check total balance across all wallets
+    let total_balance = multi_mint_wallet.total_balance().await?;
+    if total_balance == Amount::ZERO {
+        bail!("No funds available");
+    }
+
+    // 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 {
+            bail!(
+                "Source mint {} is not in the wallet. Please add it first.",
+                url
+            );
+        }
+        url
+    } else {
+        // Show available mints and let user select source
+        select_mint(
+            multi_mint_wallet,
+            "Enter source mint number to transfer from",
+            None,
+        )
+        .await?
+    };
+
+    // Get target mint URL either from args or by prompting user
+    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 {
+            bail!(
+                "Target mint {} is not in the wallet. Please add it first.",
+                url
+            );
+        }
+        url
+    } else {
+        // Show available mints (excluding source) and let user select target
+        select_mint(
+            multi_mint_wallet,
+            "Enter target mint number to transfer to",
+            Some(&source_mint_url),
+        )
+        .await?
+    };
+
+    // Ensure source and target are different
+    if source_mint_url == target_mint_url {
+        bail!("Source and target mints must be different");
+    }
+
+    // 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);
+
+    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 {
+        println!(
+            "\nTransferring full balance ({} {}) from {} to {}...",
+            source_balance,
+            multi_mint_wallet.unit(),
+            source_mint_url,
+            target_mint_url
+        );
+        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()
+            ))?),
+        };
+
+        if source_balance < amount {
+            bail!(
+                "Insufficient funds in source mint. Available: {} {}, Required: {} {}",
+                source_balance,
+                multi_mint_wallet.unit(),
+                amount,
+                multi_mint_wallet.unit()
+            );
+        }
+
+        println!(
+            "\nTransferring {} {} from {} to {}...",
+            amount,
+            multi_mint_wallet.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 {
+        println!(
+            "Fees paid: {} {}",
+            transfer_result.fees_paid,
+            multi_mint_wallet.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(())
+}

+ 1 - 6
crates/cdk-cli/src/sub_commands/update_mint_url.rs

@@ -1,7 +1,5 @@
 use anyhow::{anyhow, Result};
 use cdk::mint_url::MintUrl;
-use cdk::nuts::CurrencyUnit;
-use cdk::wallet::types::WalletKey;
 use cdk::wallet::MultiMintWallet;
 use clap::Args;
 
@@ -23,10 +21,7 @@ pub async fn update_mint_url(
     } = sub_command_args;
 
     let mut wallet = multi_mint_wallet
-        .get_wallet(&WalletKey::new(
-            sub_command_args.old_mint_url.clone(),
-            CurrencyUnit::Sat,
-        ))
+        .get_wallet(&sub_command_args.old_mint_url)
         .await
         .ok_or(anyhow!("Unknown mint url"))?
         .clone();

+ 7 - 62
crates/cdk-cli/src/utils.rs

@@ -1,12 +1,9 @@
 use std::io::{self, Write};
 use std::str::FromStr;
 
-use anyhow::{bail, Result};
+use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::nuts::CurrencyUnit;
 use cdk::wallet::multi_mint_wallet::MultiMintWallet;
-use cdk::wallet::types::WalletKey;
-use cdk::Amount;
 
 /// Helper function to get user input with a prompt
 pub fn get_user_input(prompt: &str) -> Result<String> {
@@ -28,73 +25,21 @@ where
     Ok(number)
 }
 
-/// Helper function to validate a mint number against available mints
-pub fn validate_mint_number(mint_number: usize, mint_count: usize) -> Result<()> {
-    if mint_number >= mint_count {
-        bail!("Invalid mint number");
-    }
-    Ok(())
-}
-
-/// Helper function to check if there are enough funds for an operation
-pub fn check_sufficient_funds(available: Amount, required: Amount) -> Result<()> {
-    if required.gt(&available) {
-        bail!("Not enough funds");
-    }
-    Ok(())
-}
-
-/// Helper function to get a wallet from the multi-mint wallet by mint URL
-pub async fn get_wallet_by_mint_url(
-    multi_mint_wallet: &MultiMintWallet,
-    mint_url_str: &str,
-    unit: CurrencyUnit,
-) -> Result<cdk::wallet::Wallet> {
-    let mint_url = MintUrl::from_str(mint_url_str)?;
-
-    let wallet_key = WalletKey::new(mint_url.clone(), unit);
-    let wallet = multi_mint_wallet
-        .get_wallet(&wallet_key)
-        .await
-        .ok_or_else(|| anyhow::anyhow!("Wallet not found for mint URL: {}", mint_url_str))?;
-
-    Ok(wallet.clone())
-}
-
-/// Helper function to get a wallet from the multi-mint wallet
-pub async fn get_wallet_by_index(
-    multi_mint_wallet: &MultiMintWallet,
-    mint_amounts: &[(MintUrl, Amount)],
-    mint_number: usize,
-    unit: CurrencyUnit,
-) -> Result<cdk::wallet::Wallet> {
-    validate_mint_number(mint_number, mint_amounts.len())?;
-
-    let wallet_key = WalletKey::new(mint_amounts[mint_number].0.clone(), unit);
-    let wallet = multi_mint_wallet
-        .get_wallet(&wallet_key)
-        .await
-        .ok_or_else(|| anyhow::anyhow!("Wallet not found"))?;
-
-    Ok(wallet.clone())
-}
-
 /// Helper function to create or get a wallet
 pub async fn get_or_create_wallet(
     multi_mint_wallet: &MultiMintWallet,
     mint_url: &MintUrl,
-    unit: CurrencyUnit,
 ) -> Result<cdk::wallet::Wallet> {
-    match multi_mint_wallet
-        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
-        .await
-    {
+    match multi_mint_wallet.get_wallet(mint_url).await {
         Some(wallet) => Ok(wallet.clone()),
         None => {
             tracing::debug!("Wallet does not exist creating..");
-            multi_mint_wallet
-                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
+            Ok(multi_mint_wallet
+                .get_wallet(mint_url)
                 .await
+                .expect("Wallet should exist after adding mint")
+                .clone())
         }
     }
 }

+ 26 - 0
crates/cdk-common/src/error.rs

@@ -250,6 +250,32 @@ pub enum Error {
     /// Preimage not provided
     #[error("Preimage not provided")]
     PreimageNotProvided,
+
+    // MultiMint Wallet Errors
+    /// Currency unit mismatch in MultiMintWallet
+    #[error("Currency unit mismatch: wallet uses {expected}, but {found} provided")]
+    MultiMintCurrencyUnitMismatch {
+        /// Expected currency unit
+        expected: CurrencyUnit,
+        /// Found currency unit
+        found: CurrencyUnit,
+    },
+    /// Unknown mint in MultiMintWallet
+    #[error("Unknown mint: {mint_url}")]
+    UnknownMint {
+        /// URL of the unknown mint
+        mint_url: String,
+    },
+    /// Transfer between mints timed out
+    #[error("Transfer timeout: failed to transfer {amount} from {source_mint} to {target_mint}")]
+    TransferTimeout {
+        /// Source mint URL
+        source_mint: String,
+        /// Target mint URL  
+        target_mint: String,
+        /// Amount that failed to transfer
+        amount: Amount,
+    },
     /// Insufficient Funds
     #[error("Insufficient funds")]
     InsufficientFunds,

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

@@ -4,11 +4,13 @@
 
 pub mod database;
 pub mod error;
+pub mod multi_mint_wallet;
 pub mod types;
 pub mod wallet;
 
 pub use database::*;
 pub use error::*;
+pub use multi_mint_wallet::*;
 pub use types::*;
 pub use wallet::*;
 

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

@@ -0,0 +1,416 @@
+//! 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,
+    TransferMode as CdkTransferMode, TransferResult as CdkTransferResult,
+};
+
+use crate::error::FfiError;
+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 WalletDatabase trait
+    #[uniffi::constructor]
+    pub async 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::InvalidMnemonic { msg: e.to_string() })?;
+        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 = CdkMultiMintWallet::new(localstore, seed, unit.into()).await?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Create a new MultiMintWallet with proxy configuration
+    #[uniffi::constructor]
+    pub async 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::InvalidMnemonic { msg: e.to_string() })?;
+        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::InvalidUrl { msg: e.to_string() })?;
+
+        let wallet =
+            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()
+    }
+
+    /// 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()?;
+        self.inner
+            .add_mint(cdk_mint_url, target_proof_count.map(|c| c as usize))
+            .await?;
+        Ok(())
+    }
+
+    /// Remove mint from MultiMintWallet
+    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").unwrap()
+            })
+        });
+        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
+        }
+    }
+
+    /// 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<Arc<Proof>> = mint_proofs
+                .into_iter()
+                .map(|p| Arc::new(p.into()))
+                .collect();
+            proofs_by_mint.insert(mint_url.to_string(), ffi_proofs);
+        }
+        Ok(proofs_by_mint)
+    }
+
+    /// 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<Amount, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let amount = self.inner.restore(&cdk_mint_url).await?;
+        Ok(amount.into())
+    }
+
+    /// Prepare a send operation from a specific mint
+    pub async fn prepare_send(
+        &self,
+        mint_url: MintUrl,
+        amount: Amount,
+        options: MultiMintSendOptions,
+    ) -> Result<Arc<PreparedSend>, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let prepared = self
+            .inner
+            .prepare_send(cdk_mint_url, amount.into(), options.into())
+            .await?;
+        Ok(Arc::new(prepared.into()))
+    }
+
+    /// Get a mint quote from a specific mint
+    pub async fn mint_quote(
+        &self,
+        mint_url: MintUrl,
+        amount: Amount,
+        description: 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, amount.into(), description)
+            .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, conditions)
+            .await?;
+        Ok(proofs.into_iter().map(|p| Arc::new(p.into())).collect())
+    }
+
+    /// Get a melt quote from a specific mint
+    pub async fn melt_quote(
+        &self,
+        mint_url: MintUrl,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> 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, request, cdk_options)
+            .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<Melted, FfiError> {
+        let cdk_options = options.map(Into::into);
+        let cdk_max_fee = max_fee.map(Into::into);
+        let melted = self.inner.melt(&bolt11, cdk_options, cdk_max_fee).await?;
+        Ok(melted.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| Arc::new(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())
+    }
+
+    /// Check all mint quotes and mint if paid
+    pub async fn check_all_mint_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.check_all_mint_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()
+    }
+
+    /// 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(())
+    }
+}
+
+/// 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(),
+        }
+    }
+}
+
+/// 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
+    }
+}
+
+/// 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<Arc<Proof>>>;

+ 6 - 0
crates/cdk/src/wallet/builder.rs

@@ -111,6 +111,12 @@ impl WalletBuilder {
         self
     }
 
+    /// Set a custom client connector from Arc
+    pub fn shared_client(mut self, client: Arc<dyn MintConnector + Send + Sync>) -> Self {
+        self.client = Some(client);
+        self
+    }
+
     /// Set auth CAT (Clear Auth Token)
     #[cfg(feature = "auth")]
     pub fn set_auth_cat(mut self, cat: String) -> Self {

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

@@ -61,7 +61,7 @@ pub use mint_connector::transport::Transport as HttpTransport;
 #[cfg(feature = "auth")]
 pub use mint_connector::AuthHttpClient;
 pub use mint_connector::{HttpClient, MintConnector};
-pub use multi_mint_wallet::MultiMintWallet;
+pub use multi_mint_wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet};
 pub use receive::ReceiveOptions;
 pub use send::{PreparedSend, SendMemo, SendOptions};
 pub use types::{MeltQuote, MintQuote, SendKind};

Fichier diff supprimé car celui-ci est trop grand
+ 748 - 137
crates/cdk/src/wallet/multi_mint_wallet.rs


+ 5 - 5
crates/cdk/src/wallet/payment_request.rs

@@ -22,7 +22,7 @@ use crate::nuts::nut11::{Conditions, SigFlag, SpendingConditions};
 use crate::nuts::nut18::Nut10SecretRequest;
 use crate::nuts::{CurrencyUnit, Transport};
 #[cfg(feature = "nostr")]
-use crate::wallet::ReceiveOptions;
+use crate::wallet::MultiMintReceiveOptions;
 use crate::wallet::{MultiMintWallet, SendOptions};
 use crate::Wallet;
 
@@ -356,7 +356,7 @@ impl MultiMintWallet {
     ) -> Result<(PaymentRequest, Option<NostrWaitInfo>), Error> {
         // Collect available mints for the selected unit
         let mints = self
-            .get_balances(&CurrencyUnit::from_str(&params.unit)?)
+            .get_balances()
             .await?
             .keys()
             .cloned()
@@ -458,7 +458,7 @@ impl MultiMintWallet {
     ) -> Result<PaymentRequest, Error> {
         // Collect available mints for the selected unit
         let mints = self
-            .get_balances(&CurrencyUnit::from_str(&params.unit)?)
+            .get_balances()
             .await?
             .keys()
             .cloned()
@@ -536,7 +536,7 @@ impl MultiMintWallet {
                     );
 
                     let amount = self
-                        .receive(&token.to_string(), ReceiveOptions::default())
+                        .receive(&token.to_string(), MultiMintReceiveOptions::default())
                         .await?;
 
                     // Stop after first successful receipt
@@ -602,7 +602,7 @@ impl MultiMintWallet {
                                 );
 
                                 let amount = self
-                                    .receive(&token.to_string(), ReceiveOptions::default())
+                                    .receive(&token.to_string(), MultiMintReceiveOptions::default())
                                     .await?;
 
                                 return Ok(amount);

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff