Mpp cdk cli (#743)

* refactor: Extract user input logic into a helper function

* feat: get multiple quotes (hacky)

* refactor: cdk-cli

* refactor: cdk-cli

* feat: refactor balances
This commit is contained in:
thesimplekid
2025-05-05 08:41:54 +01:00
committed by GitHub
parent c1d6880daa
commit 34eb10fd9e
7 changed files with 254 additions and 126 deletions

View File

@@ -20,6 +20,7 @@ use url::Url;
mod nostr_storage;
mod sub_commands;
mod token_storage;
mod utils;
const DEFAULT_WORK_DIR: &str = ".cdk-cli";
@@ -183,7 +184,17 @@ async fn main() -> Result<()> {
let wallet = builder.build()?;
wallet.get_mint_info().await?;
let wallet_clone = wallet.clone();
tokio::spawn(async move {
if let Err(err) = wallet_clone.get_mint_info().await {
tracing::error!(
"Could not get mint quote for {}, {}",
wallet_clone.mint_url,
err
);
}
});
wallets.push(wallet);
}

View File

@@ -19,7 +19,11 @@ pub async fn mint_balances(
let mut wallets_vec = Vec::with_capacity(wallets.len());
for (i, (mint_url, amount)) in wallets.iter().enumerate() {
for (i, (mint_url, amount)) in wallets
.iter()
.filter(|(_, a)| a > &&Amount::ZERO)
.enumerate()
{
let mint_url = mint_url.clone();
println!("{i}: {mint_url} {amount} {unit}");
wallets_vec.push((mint_url, *amount))

View File

@@ -1,5 +1,3 @@
use std::io;
use std::io::Write;
use std::str::FromStr;
use anyhow::{bail, Result};
@@ -9,8 +7,10 @@ use cdk::wallet::multi_mint_wallet::MultiMintWallet;
use cdk::wallet::types::WalletKey;
use cdk::Bolt11Invoice;
use clap::Args;
use tokio::task::JoinSet;
use crate::sub_commands::balance::mint_balances;
use crate::utils::{get_number_input, get_user_input, get_wallet_by_index, validate_mint_number};
#[derive(Args)]
pub struct MeltSubCommand {
@@ -29,82 +29,148 @@ pub async fn pay(
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
println!("Enter mint number to melt from");
let mut mints = vec![];
let mut mint_amounts = vec![];
if sub_command_args.mpp {
loop {
let mint_number: String =
get_user_input("Enter mint number to melt from and -1 when done.")?;
let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let mint_number: usize = user_input.trim().parse()?;
if mint_number.gt(&(mints_amounts.len() - 1)) {
bail!("Invalid mint number");
}
let wallet = mints_amounts[mint_number].0.clone();
let wallet = multi_mint_wallet
.get_wallet(&WalletKey::new(wallet, unit))
.await
.expect("Known wallet");
println!("Enter bolt11 invoice request");
let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
let available_funds =
<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT;
// Determine payment amount and options
let options = if sub_command_args.mpp || bolt11.amount_milli_satoshis().is_none() {
// Get user input for amount
println!(
"Enter the amount you would like to pay in sats for a {} payment.",
if sub_command_args.mpp {
"MPP"
} else {
"amountless invoice"
if mint_number == "-1" || mint_number.is_empty() {
break;
}
);
let mut user_input = String::new();
io::stdout().flush()?;
io::stdin().read_line(&mut user_input)?;
let mint_number: usize = mint_number.parse()?;
validate_mint_number(mint_number, mints_amounts.len())?;
let user_amount = user_input.trim_end().parse::<u64>()? * MSAT_IN_SAT;
if user_amount > available_funds {
bail!("Not enough funds");
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);
}
Some(if sub_command_args.mpp {
MeltOptions::new_mpp(user_amount)
} else {
MeltOptions::new_amountless(user_amount)
})
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
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 = quotes.join_all().await;
for (wallet, quote) in quotes.iter() {
if let Err(quote) = quote {
tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote);
bail!("Could not get melt quote for {}", wallet.mint_url);
} else {
let quote = quote.as_ref().unwrap();
println!(
"Melt quote {} for mint {} of amount {} with fee {}.",
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
);
}
}
let mut melts = JoinSet::new();
for (wallet, quote) in quotes {
let quote = quote.expect("Errors checked above");
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");
}
} else {
// Check if invoice amount exceeds available funds
let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
if invoice_amount > available_funds {
bail!("Not enough funds");
let mint_number: usize = get_number_input("Enter mint number to melt from")?;
let wallet =
get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
.await?;
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
let available_funds =
<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT;
// Determine payment amount and options
let options = if bolt11.amount_milli_satoshis().is_none() {
// Get user input for amount
let prompt = format!(
"Enter the amount you would like to pay in sats for a {} payment.",
if sub_command_args.mpp {
"MPP"
} else {
"amountless invoice"
}
);
let user_amount = get_number_input::<u64>(&prompt)? * MSAT_IN_SAT;
if user_amount > available_funds {
bail!("Not enough funds");
}
Some(MeltOptions::new_amountless(user_amount))
} else {
// Check if invoice amount exceeds available funds
let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
if invoice_amount > available_funds {
bail!("Not enough funds");
}
None
};
// Process payment
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
println!("{:?}", quote);
let melt = wallet.melt(&quote.id).await?;
println!("Paid invoice: {}", melt.state);
if let Some(preimage) = melt.preimage {
println!("Payment preimage: {}", preimage);
}
None
};
// Process payment
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
println!("{:?}", quote);
let melt = wallet.melt(&quote.id).await?;
println!("Paid invoice: {}", melt.state);
if let Some(preimage) = melt.preimage {
println!("Payment preimage: {}", preimage);
}
Ok(())

View File

@@ -5,12 +5,13 @@ use cdk::amount::SplitTarget;
use cdk::mint_url::MintUrl;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
use cdk::wallet::types::WalletKey;
use cdk::wallet::{MultiMintWallet, WalletSubscription};
use cdk::Amount;
use clap::Args;
use serde::{Deserialize, Serialize};
use crate::utils::get_or_create_wallet;
#[derive(Args, Serialize, Deserialize)]
pub struct MintSubCommand {
/// Mint url
@@ -36,18 +37,7 @@ pub async fn mint(
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
let description: Option<String> = sub_command_args.description.clone();
let wallet = match multi_mint_wallet
.get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
.await
{
Some(wallet) => wallet.clone(),
None => {
tracing::debug!("Wallet does not exist creating..");
multi_mint_wallet
.create_and_add_wallet(&mint_url.to_string(), unit, None)
.await?
}
};
let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
let quote_id = match &sub_command_args.quote_id {
None => {

View File

@@ -14,6 +14,7 @@ use nostr_sdk::nips::nip04;
use nostr_sdk::{Filter, Keys, Kind, Timestamp};
use crate::nostr_storage;
use crate::utils::get_or_create_wallet;
#[derive(Args)]
pub struct ReceiveSubCommand {
@@ -137,17 +138,14 @@ async fn receive_token(
let token: Token = Token::from_str(token_str)?;
let mint_url = token.mint_url()?;
let unit = token.unit().unwrap_or_default();
let wallet_key = WalletKey::new(mint_url.clone(), token.unit().unwrap_or_default());
if multi_mint_wallet.get_wallet(&wallet_key).await.is_none() {
multi_mint_wallet
.create_and_add_wallet(
&mint_url.to_string(),
token.unit().unwrap_or_default(),
None,
)
.await?;
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?;
}
let amount = multi_mint_wallet

View File

@@ -1,15 +1,14 @@
use std::io;
use std::io::Write;
use std::str::FromStr;
use anyhow::{bail, Result};
use anyhow::Result;
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
use cdk::wallet::types::{SendKind, WalletKey};
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};
#[derive(Args)]
pub struct SendSubCommand {
@@ -55,30 +54,13 @@ pub async fn send(
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
println!("Enter mint number to create token");
let mint_number: usize = get_number_input("Enter mint number to create token")?;
let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let wallet = get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await?;
let mint_number: usize = user_input.trim().parse()?;
let token_amount = Amount::from(get_number_input::<u64>("Enter value of token in sats")?);
if mint_number.gt(&(mints_amounts.len() - 1)) {
bail!("Invalid mint number");
}
println!("Enter value of token in sats");
let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let token_amount = Amount::from(user_input.trim().parse::<u64>()?);
if token_amount.gt(&mints_amounts[mint_number].1) {
bail!("Not enough funds");
}
check_sufficient_funds(mints_amounts[mint_number].1, token_amount)?;
let conditions = match &sub_command_args.preimage {
Some(preimage) => {
@@ -156,12 +138,6 @@ pub async fn send(
},
};
let wallet = mints_amounts[mint_number].0.clone();
let wallet = multi_mint_wallet
.get_wallet(&WalletKey::new(wallet, unit))
.await
.expect("Known wallet");
let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
(true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
(true, None) => SendKind::OfflineExact,

View File

@@ -0,0 +1,83 @@
use std::io::{self, Write};
use std::str::FromStr;
use anyhow::{bail, 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> {
println!("{}", prompt);
let mut user_input = String::new();
io::stdout().flush()?;
io::stdin().read_line(&mut user_input)?;
Ok(user_input.trim().to_string())
}
/// Helper function to get a number from user input with a prompt
pub fn get_number_input<T>(prompt: &str) -> Result<T>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{
let input = get_user_input(prompt)?;
let number = input.parse::<T>()?;
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
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
{
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)
.await
}
}
}