diff --git a/crates/cdk-cli/src/sub_commands/pay_request.rs b/crates/cdk-cli/src/sub_commands/pay_request.rs index dc4bddec..6ceeb258 100644 --- a/crates/cdk-cli/src/sub_commands/pay_request.rs +++ b/crates/cdk-cli/src/sub_commands/pay_request.rs @@ -92,7 +92,7 @@ pub async fn pay_request( ) .await?; - let token = matching_wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; // We need the keysets information to properly convert from token proof to proof let keysets_info = match matching_wallet diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index 5d177547..e1b37bff 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -221,7 +221,7 @@ pub async fn send( }, ) .await?; - let token = wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; match sub_command_args.v3 { true => { diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index d49bd2a8..7ed954b8 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/crates/cdk-integration-tests/tests/integration_tests_pure.rs @@ -70,11 +70,8 @@ async fn test_swap_to_send() { .expect("Failed to get ys") ) ); - let token = wallet_alice - .send( - prepared_send, - Some(SendMemo::for_token("test_swapt_to_send")), - ) + let token = prepared_send + .confirm(Some(SendMemo::for_token("test_swapt_to_send"))) .await .expect("Failed to send token"); let keysets_info = wallet_alice.get_mint_keysets().await.unwrap(); diff --git a/crates/cdk-integration-tests/tests/test_fees.rs b/crates/cdk-integration-tests/tests/test_fees.rs index 0893ab41..1433030a 100644 --- a/crates/cdk-integration-tests/tests/test_fees.rs +++ b/crates/cdk-integration-tests/tests/test_fees.rs @@ -64,7 +64,7 @@ async fn test_swap() { assert_eq!(fee, 1.into()); - let send = wallet.send(send, None).await.unwrap(); + let send = send.confirm(None).await.unwrap(); let rec_amount = wallet .receive(&send.to_string(), ReceiveOptions::default()) diff --git a/crates/cdk/README.md b/crates/cdk/README.md index 670a9047..5ed3d02e 100644 --- a/crates/cdk/README.md +++ b/crates/cdk/README.md @@ -101,7 +101,7 @@ async fn main() { // Send the token let prepared_send = wallet.prepare_send(Amount::ONE, SendOptions::default()).await.unwrap(); - let token = wallet.send(prepared_send, None).await.unwrap(); + let token = prepared_send.confirm(None).await.unwrap(); println!("{}", token); } diff --git a/crates/cdk/examples/auth_wallet.rs b/crates/cdk/examples/auth_wallet.rs index 4eff20ac..6d4c5533 100644 --- a/crates/cdk/examples/auth_wallet.rs +++ b/crates/cdk/examples/auth_wallet.rs @@ -88,7 +88,7 @@ async fn main() -> Result<(), Error> { let prepared_send = wallet .prepare_send(10.into(), SendOptions::default()) .await?; - let token = wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; println!("Created token: {}", token); diff --git a/crates/cdk/examples/mint-token.rs b/crates/cdk/examples/mint-token.rs index 0c9bf466..9a5dd7ae 100644 --- a/crates/cdk/examples/mint-token.rs +++ b/crates/cdk/examples/mint-token.rs @@ -62,7 +62,7 @@ async fn main() -> Result<(), Error> { // Send a token with the specified amount let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?; - let token = wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; println!("Token:"); println!("{}", token); diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 2fb741e2..64cce6ac 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -87,7 +87,7 @@ async fn main() -> Result<(), Error> { ) .await?; println!("Fee: {}", prepared_send.fee()); - let token = wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; println!("Created token locked to pubkey: {}", secret.public_key()); println!("{}", token); diff --git a/crates/cdk/examples/wallet.rs b/crates/cdk/examples/wallet.rs index 3bffe8ef..890cb4d6 100644 --- a/crates/cdk/examples/wallet.rs +++ b/crates/cdk/examples/wallet.rs @@ -59,7 +59,7 @@ async fn main() -> Result<(), Box> { // Send the token let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?; - let token = wallet.send(prepared_send, None).await?; + let token = prepared_send.confirm(None).await?; println!("{}", token); diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index cfac4e1b..310cc214 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -15,7 +15,7 @@ use tokio::sync::RwLock; use tracing::instrument; use super::receive::ReceiveOptions; -use super::send::{PreparedSend, SendMemo, SendOptions}; +use super::send::{PreparedSend, SendOptions}; use super::Error; use crate::amount::SplitTarget; use crate::mint_url::MintUrl; @@ -177,22 +177,6 @@ impl MultiMintWallet { wallet.prepare_send(amount, opts).await } - /// Create cashu token - #[instrument(skip(self))] - pub async fn send( - &self, - wallet_key: &WalletKey, - send: PreparedSend, - memo: Option, - ) -> Result { - let wallets = self.wallets.read().await; - let wallet = wallets - .get(wallet_key) - .ok_or(Error::UnknownWallet(wallet_key.clone()))?; - - wallet.send(send, memo).await - } - /// Mint quote for wallet #[instrument(skip(self))] pub async fn mint_quote( diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index 9d6b4764..190749ba 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -21,7 +21,7 @@ impl Wallet { /// ```no_compile /// let send = wallet.prepare_send(Amount::from(10), SendOptions::default()).await?; /// assert!(send.fee() <= Amount::from(1)); - /// let token = wallet.send(send, None).await?; + /// let token = send.confirm(None).await?; /// ``` #[instrument(skip(self), err)] pub async fn prepare_send( @@ -188,6 +188,7 @@ impl Wallet { // Return prepared send Ok(PreparedSend { + wallet: self.clone(), amount, options: opts, proofs_to_swap, @@ -196,135 +197,11 @@ impl Wallet { send_fee, }) } - - /// Finalize A Send Transaction - /// - /// This function finalizes a send transaction by constructing a token the [`PreparedSend`]. - /// See [`Wallet::prepare_send`] for more information. - #[instrument(skip(self), err)] - pub async fn send(&self, send: PreparedSend, memo: Option) -> Result { - tracing::info!("Sending prepared send"); - let total_send_fee = send.fee(); - let mut proofs_to_send = send.proofs_to_send; - - // Get active keyset ID - let active_keyset_id = self.fetch_active_keyset().await?.id; - tracing::debug!("Active keyset ID: {:?}", active_keyset_id); - - // Get keyset fees - let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?; - tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk); - - // Calculate total send amount - let total_send_amount = send.amount + send.send_fee; - tracing::debug!("Total send amount: {}", total_send_amount); - - // Swap proofs if necessary - if !send.proofs_to_swap.is_empty() { - let swap_amount = total_send_amount - proofs_to_send.total_amount()?; - tracing::debug!("Swapping proofs; swap_amount={:?}", swap_amount); - if let Some(proofs) = self - .swap( - Some(swap_amount), - SplitTarget::None, - send.proofs_to_swap, - send.options.conditions.clone(), - false, // already included in swap_amount - ) - .await? - { - proofs_to_send.extend(proofs); - } - } - tracing::debug!( - "Proofs to send: {:?}", - proofs_to_send.iter().map(|p| p.amount).collect::>() - ); - - // Check if sufficient proofs are available - if send.amount > proofs_to_send.total_amount()? { - return Err(Error::InsufficientFunds); - } - - // Check if proofs are reserved or unspent - let sendable_proof_ys = self - .get_proofs_with( - Some(vec![State::Reserved, State::Unspent]), - send.options.conditions.clone().map(|c| vec![c]), - ) - .await? - .ys()?; - if proofs_to_send - .ys()? - .iter() - .any(|y| !sendable_proof_ys.contains(y)) - { - tracing::warn!("Proofs to send are not reserved or unspent"); - return Err(Error::UnexpectedProofState); - } - - // Update proofs state to pending spent - tracing::debug!( - "Updating proofs state to pending spent: {:?}", - proofs_to_send.ys()? - ); - self.localstore - .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent) - .await?; - - // Include token memo - let send_memo = send.options.memo.or(memo); - let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None }); - - // Add transaction to store - self.localstore - .add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Outgoing, - amount: send.amount, - fee: total_send_fee, - unit: self.unit.clone(), - ys: proofs_to_send.ys()?, - timestamp: unix_time(), - memo: memo.clone(), - metadata: send.options.metadata, - }) - .await?; - - // Create and return token - Ok(Token::new( - self.mint_url.clone(), - proofs_to_send, - memo, - self.unit.clone(), - )) - } - - /// Cancel prepared send - pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> { - tracing::info!("Cancelling prepared send"); - - // Double-check proofs state - let reserved_proofs = self.get_reserved_proofs().await?.ys()?; - if !send - .proofs() - .ys()? - .iter() - .all(|y| reserved_proofs.contains(y)) - { - return Err(Error::UnexpectedProofState); - } - - self.localstore - .update_proofs_state(send.proofs().ys()?, State::Unspent) - .await?; - - Ok(()) - } } /// Prepared send pub struct PreparedSend { + wallet: Wallet, amount: Amount, options: SendOptions, proofs_to_swap: Proofs, @@ -375,6 +252,133 @@ impl PreparedSend { pub fn fee(&self) -> Amount { self.swap_fee + self.send_fee } + + /// Confirm the prepared send and create a token + #[instrument(skip(self), err)] + pub async fn confirm(self, memo: Option) -> Result { + tracing::info!("Confirming prepared send"); + let total_send_fee = self.fee(); + let mut proofs_to_send = self.proofs_to_send; + + // Get active keyset ID + let active_keyset_id = self.wallet.fetch_active_keyset().await?.id; + tracing::debug!("Active keyset ID: {:?}", active_keyset_id); + + // Get keyset fees + let keyset_fee_ppk = self.wallet.get_keyset_fees_by_id(active_keyset_id).await?; + tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk); + + // Calculate total send amount + let total_send_amount = self.amount + self.send_fee; + tracing::debug!("Total send amount: {}", total_send_amount); + + // Swap proofs if necessary + if !self.proofs_to_swap.is_empty() { + let swap_amount = total_send_amount - proofs_to_send.total_amount()?; + tracing::debug!("Swapping proofs; swap_amount={:?}", swap_amount); + if let Some(proofs) = self + .wallet + .swap( + Some(swap_amount), + SplitTarget::None, + self.proofs_to_swap, + self.options.conditions.clone(), + false, // already included in swap_amount + ) + .await? + { + proofs_to_send.extend(proofs); + } + } + tracing::debug!( + "Proofs to send: {:?}", + proofs_to_send.iter().map(|p| p.amount).collect::>() + ); + + // Check if sufficient proofs are available + if self.amount > proofs_to_send.total_amount()? { + return Err(Error::InsufficientFunds); + } + + // Check if proofs are reserved or unspent + let sendable_proof_ys = self + .wallet + .get_proofs_with( + Some(vec![State::Reserved, State::Unspent]), + self.options.conditions.clone().map(|c| vec![c]), + ) + .await? + .ys()?; + if proofs_to_send + .ys()? + .iter() + .any(|y| !sendable_proof_ys.contains(y)) + { + tracing::warn!("Proofs to send are not reserved or unspent"); + return Err(Error::UnexpectedProofState); + } + + // Update proofs state to pending spent + tracing::debug!( + "Updating proofs state to pending spent: {:?}", + proofs_to_send.ys()? + ); + self.wallet + .localstore + .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent) + .await?; + + // Include token memo + let send_memo = self.options.memo.or(memo); + let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None }); + + // Add transaction to store + self.wallet + .localstore + .add_transaction(Transaction { + mint_url: self.wallet.mint_url.clone(), + direction: TransactionDirection::Outgoing, + amount: self.amount, + fee: total_send_fee, + unit: self.wallet.unit.clone(), + ys: proofs_to_send.ys()?, + timestamp: unix_time(), + memo: memo.clone(), + metadata: self.options.metadata, + }) + .await?; + + // Create and return token + Ok(Token::new( + self.wallet.mint_url.clone(), + proofs_to_send, + memo, + self.wallet.unit.clone(), + )) + } + + /// Cancel the prepared send + pub async fn cancel(self) -> Result<(), Error> { + tracing::info!("Cancelling prepared send"); + + // Double-check proofs state + let reserved_proofs = self.wallet.get_reserved_proofs().await?.ys()?; + if !self + .proofs() + .ys()? + .iter() + .all(|y| reserved_proofs.contains(y)) + { + return Err(Error::UnexpectedProofState); + } + + self.wallet + .localstore + .update_proofs_state(self.proofs().ys()?, State::Unspent) + .await?; + + Ok(()) + } } impl Debug for PreparedSend {