refactor: update send functionality across wallet components (#925)

* refactor: update send functionality across wallet components

---------
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
David Caseria
2025-07-30 23:37:41 -04:00
committed by GitHub
parent f663a6e41c
commit 6ebcbba0c4
11 changed files with 141 additions and 156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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);

View File

@@ -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<SendMemo>,
) -> Result<Token, Error> {
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(

View File

@@ -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<SendMemo>) -> Result<Token, Error> {
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::<Vec<_>>()
);
// 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<SendMemo>) -> Result<Token, Error> {
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::<Vec<_>>()
);
// 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 {