Prechádzať zdrojové kódy

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
thesimplekid 1 mesiac pred
rodič
commit
34eb10fd9e

+ 12 - 1
crates/cdk-cli/src/main.rs

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

+ 5 - 1
crates/cdk-cli/src/sub_commands/balance.rs

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

+ 124 - 58
crates/cdk-cli/src/sub_commands/melt.rs

@@ -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)?;
+            if mint_number == "-1" || mint_number.is_empty() {
+                break;
+            }
 
-    let mint_number: usize = user_input.trim().parse()?;
+            let mint_number: usize = mint_number.parse()?;
+            validate_mint_number(mint_number, mints_amounts.len())?;
 
-    if mint_number.gt(&(mints_amounts.len() - 1)) {
-        bail!("Invalid mint number");
-    }
+            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);
+        }
+
+        let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
 
-    let wallet = mints_amounts[mint_number].0.clone();
+        let mut quotes = JoinSet::new();
 
-    let wallet = multi_mint_wallet
-        .get_wallet(&WalletKey::new(wallet, unit))
-        .await
-        .expect("Known wallet");
+        for (mint, amount) in mints.iter().zip(mint_amounts) {
+            let wallet = mints_amounts[*mint].0.clone();
 
-    println!("Enter bolt11 invoice request");
+            let wallet = multi_mint_wallet
+                .get_wallet(&WalletKey::new(wallet, unit.clone()))
+                .await
+                .expect("Known wallet");
+            let options = MeltOptions::new_mpp(amount * 1000);
 
-    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 bolt11_clone = bolt11.clone();
+
+            quotes.spawn(async move {
+                let quote = wallet
+                    .melt_quote(bolt11_clone.to_string(), Some(options))
+                    .await;
+
+                (wallet, quote)
+            });
+        }
 
-    let available_funds =
-        <cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT;
+        let quotes = quotes.join_all().await;
 
-    // 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"
+        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 {
-                "amountless invoice"
+                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 user_input = String::new();
-        io::stdout().flush()?;
-        io::stdin().read_line(&mut user_input)?;
+        let mut melts = JoinSet::new();
 
-        let user_amount = user_input.trim_end().parse::<u64>()? * MSAT_IN_SAT;
+        for (wallet, quote) in quotes {
+            let quote = quote.expect("Errors checked above");
 
-        if user_amount > available_funds {
-            bail!("Not enough funds");
+            melts.spawn(async move {
+                let melt = wallet.melt(&quote.id).await;
+                (wallet, melt)
+            });
         }
 
-        Some(if sub_command_args.mpp {
-            MeltOptions::new_mpp(user_amount)
-        } else {
-            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");
+        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;
+                }
+            }
         }
-        None
-    };
 
-    // Process payment
-    let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
-    println!("{:?}", quote);
+        if error {
+            bail!("Could not complete all melts");
+        }
+    } else {
+        let mint_number: usize = get_number_input("Enter mint number to melt from")?;
 
-    let melt = wallet.melt(&quote.id).await?;
-    println!("Paid invoice: {}", melt.state);
+        let wallet =
+            get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
+                .await?;
 
-    if let Some(preimage) = melt.preimage {
-        println!("Payment preimage: {}", preimage);
+        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);
+        }
     }
 
     Ok(())

+ 3 - 13
crates/cdk-cli/src/sub_commands/mint.rs

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

+ 9 - 11
crates/cdk-cli/src/sub_commands/receive.rs

@@ -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 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?;
+    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?;
     }
 
     let amount = multi_mint_wallet

+ 7 - 31
crates/cdk-cli/src/sub_commands/send.rs

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

+ 83 - 0
crates/cdk-cli/src/utils.rs

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