Files
cdk/crates/cdk-cli/src/sub_commands/send.rs
David Caseria 6ebcbba0c4 refactor: update send functionality across wallet components (#925)
* refactor: update send functionality across wallet components

---------
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
2025-07-30 23:37:41 -04:00

239 lines
7.7 KiB
Rust

use std::str::FromStr;
use anyhow::{anyhow, Result};
use cdk::nuts::{Conditions, CurrencyUnit, 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,
};
#[derive(Args)]
pub struct SendSubCommand {
/// Token Memo
#[arg(short, long)]
memo: Option<String>,
/// Preimage
#[arg(long, conflicts_with = "hash")]
preimage: Option<String>,
/// Hash for HTLC (alternative to preimage)
#[arg(long, conflicts_with = "preimage")]
hash: Option<String>,
/// Required number of signatures
#[arg(long)]
required_sigs: Option<u64>,
/// Locktime before refund keys can be used
#[arg(short, long)]
locktime: Option<u64>,
/// Pubkey to lock proofs to
#[arg(short, long, action = clap::ArgAction::Append)]
pubkey: Vec<String>,
/// Refund keys that can be used after locktime
#[arg(long, action = clap::ArgAction::Append)]
refund_keys: Vec<String>,
/// Token as V3 token
#[arg(short, long)]
v3: bool,
/// Should the send be offline only
#[arg(short, long)]
offline: bool,
/// Include fee to redeem in token
#[arg(short, long)]
include_fee: bool,
/// Amount willing to overpay to avoid a swap
#[arg(short, long)]
tolerance: Option<u64>,
/// Mint URL to use for sending
#[arg(long)]
mint_url: Option<String>,
/// Currency unit e.g. sat
#[arg(default_value = "sat")]
unit: String,
}
pub async fn send(
multi_mint_wallet: &MultiMintWallet,
sub_command_args: &SendSubCommand,
) -> Result<()> {
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
// Get wallet either by mint URL or by index
let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
// Use the provided mint URL
get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit).await?
} else {
// Fallback to the index-based selection
let mint_number: usize = get_number_input("Enter mint number to create token")?;
get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await?
};
let token_amount = Amount::from(get_number_input::<u64>("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)?;
let conditions = match (&sub_command_args.preimage, &sub_command_args.hash) {
(Some(_), Some(_)) => {
// This case shouldn't be reached due to Clap's conflicts_with attribute
unreachable!("Both preimage and hash were provided despite conflicts_with attribute")
}
(Some(preimage), None) => {
let pubkeys = match sub_command_args.pubkey.is_empty() {
true => None,
false => Some(
sub_command_args
.pubkey
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect(),
),
};
let refund_keys = match sub_command_args.refund_keys.is_empty() {
true => None,
false => Some(
sub_command_args
.refund_keys
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect(),
),
};
let conditions = Conditions::new(
sub_command_args.locktime,
pubkeys,
refund_keys,
sub_command_args.required_sigs,
None,
None,
)
.unwrap();
Some(SpendingConditions::new_htlc(
preimage.clone(),
Some(conditions),
)?)
}
(None, Some(hash)) => {
let pubkeys = match sub_command_args.pubkey.is_empty() {
true => None,
false => Some(
sub_command_args
.pubkey
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect(),
),
};
let refund_keys = match sub_command_args.refund_keys.is_empty() {
true => None,
false => Some(
sub_command_args
.refund_keys
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect(),
),
};
let conditions = Conditions::new(
sub_command_args.locktime,
pubkeys,
refund_keys,
sub_command_args.required_sigs,
None,
None,
)?;
Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?)
}
(None, None) => match sub_command_args.pubkey.is_empty() {
true => None,
false => {
let pubkeys: Vec<PublicKey> = sub_command_args
.pubkey
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect();
let refund_keys: Vec<PublicKey> = sub_command_args
.refund_keys
.iter()
.map(|p| PublicKey::from_str(p).unwrap())
.collect();
let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys);
let data_pubkey = pubkeys[0];
let pubkeys = pubkeys[1..].to_vec();
let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys);
let conditions = Conditions::new(
sub_command_args.locktime,
pubkeys,
refund_keys,
sub_command_args.required_sigs,
None,
None,
)?;
Some(SpendingConditions::P2PKConditions {
data: data_pubkey,
conditions: Some(conditions),
})
}
},
};
let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
(true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
(true, None) => SendKind::OfflineExact,
(false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
(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?;
match sub_command_args.v3 {
true => {
let token = token;
println!("{}", token.to_v3_string());
}
false => {
println!("{token}");
}
}
Ok(())
}