mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-02 03:35:57 +01:00
MultiMintWallet Refactor (#1001)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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 => {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("e.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 "es {
|
||||
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, "e.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("e.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("e.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("e.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(())
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
209
crates/cdk-cli/src/sub_commands/transfer.rs
Normal file
209
crates/cdk-cli/src/sub_commands/transfer.rs
Normal 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(())
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
crates/cdk-ffi/src/multi_mint_wallet.rs
Normal file
416
crates/cdk-ffi/src/multi_mint_wallet.rs
Normal file
@@ -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, "e_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>>>;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(¶ms.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(¶ms.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);
|
||||
|
||||
Reference in New Issue
Block a user