MultiMintWallet Refactor (#1001)

This commit is contained in:
David Caseria
2025-09-17 12:27:54 -04:00
committed by GitHub
parent 62fcb1730e
commit 23cba67c3b
24 changed files with 2384 additions and 670 deletions

View File

@@ -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
}

View File

@@ -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());

View File

@@ -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 => {

View File

@@ -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")
}
};

View File

@@ -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()
}
};

View File

@@ -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,

View File

@@ -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)?;
let bolt11_str = get_user_input("Enter bolt11 invoice")?;
let _bolt11 = Bolt11Invoice::from_str(&bolt11_str)?; // Validate invoice format
// Process BOLT11 MPP payment
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
// 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()
);
}
// Get quotes from all mints
let quotes = get_mpp_quotes(
multi_mint_wallet,
&mints_amounts,
&mints,
&mint_amounts,
&unit,
&bolt11,
)
.await?;
// 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)")?;
// 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?
};
if mint_input.to_lowercase() == "done" || mint_input.is_empty() {
break;
}
// Find the mint amount for the selected wallet to check available funds
let mint_url = &wallet.mint_url;
let mint_amount = mints_amounts
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)?;
// Process payment
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
process_payment(&wallet, quote).await?;
// 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?
};
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?;
// 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);
}
}
}
}
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;
// Validate all quotes succeeded
let mut valid_quotes = Vec::new();
for (wallet, quote_result) in quotes_results {
match quote_result {
Ok(quote) => {
println!(
"Melt quote {} for mint {} of amount {} with fee {}.",
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
);
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);
}
}
}
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(())
}

View File

@@ -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)?;

View File

@@ -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()
}
};

View File

@@ -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;

View File

@@ -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(())
}

View File

@@ -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)
}

View File

@@ -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()
}
};

View File

@@ -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?;
let token_amount = Amount::from(get_number_input::<u64>(&format!(
"Enter value of token in {}",
multi_mint_wallet.unit()
))?);
// 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>("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 => {

View File

@@ -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(())
}

View File

@@ -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();

View File

@@ -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())
}
}
}