diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index f54e7aed..66efc793 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -385,7 +385,7 @@ impl Wallet { Ok(()) } - /// Create Split Payload + /// Create Swap Payload async fn create_swap( &mut self, mint_url: &UncheckedUrl, @@ -393,7 +393,7 @@ impl Wallet { amount: Option, proofs: Proofs, ) -> Result { - // Since split is used to get the needed combination of tokens for a specific + // Since swap is used to get the needed combination of tokens for a specific // amount first blinded messages are created for the amount let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?.unwrap(); @@ -510,6 +510,9 @@ impl Wallet { self.localstore .add_pending_proofs(mint_url.clone(), proofs) .await?; + self.localstore + .add_pending_proofs(mint_url.clone(), send_proofs.clone()) + .await?; self.localstore .add_proofs(mint_url.clone(), keep_proofs) diff --git a/crates/cashu/src/error.rs b/crates/cashu/src/error.rs index b43e3e71..6bb9aaae 100644 --- a/crates/cashu/src/error.rs +++ b/crates/cashu/src/error.rs @@ -48,6 +48,8 @@ pub enum Error { Key, #[error("Invalid signature")] InvalidSignature, + #[error("Locktime in past")] + LocktimeInPast, /// Custom error #[error("`{0}`")] CustomError(String), diff --git a/crates/cashu/src/nuts/nut00.rs b/crates/cashu/src/nuts/nut00.rs index df98b264..f6452720 100644 --- a/crates/cashu/src/nuts/nut00.rs +++ b/crates/cashu/src/nuts/nut00.rs @@ -100,7 +100,7 @@ pub mod wallet { use super::{CurrencyUnit, MintProofs}; use crate::dhke::blind_message; use crate::error::wallet; - use crate::nuts::{BlindedMessage, Id, Proofs, SecretKey}; + use crate::nuts::{BlindedMessage, Id, P2PKConditions, Proofs, SecretKey}; use crate::secret::Secret; use crate::url::UncheckedUrl; use crate::{error, Amount}; @@ -134,28 +134,6 @@ pub mod wallet { secrets: Vec, } - // Implement Iterator for PreMintSecrets - impl Iterator for PreMintSecrets { - type Item = PreMint; - - fn next(&mut self) -> Option { - // Use the iterator of the vector - self.secrets.pop() - } - } - - impl Ord for PreMintSecrets { - fn cmp(&self, other: &Self) -> Ordering { - self.secrets.cmp(&other.secrets) - } - } - - impl PartialOrd for PreMintSecrets { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - impl PreMintSecrets { /// Outputs for speceifed amount with random secret pub fn random(keyset_id: Id, amount: Amount) -> Result { @@ -278,6 +256,37 @@ pub mod wallet { Ok(pre_mint_secrets) } + #[cfg(feature = "nut11")] + pub fn with_p2pk_conditions( + keyset_id: Id, + amount: Amount, + conditions: P2PKConditions, + ) -> Result { + let amount_split = amount.split(); + + let mut output = Vec::with_capacity(amount_split.len()); + + for amount in amount_split { + let secret: Secret = conditions.clone().try_into().unwrap(); + let (blinded, r) = blind_message(&secret.to_bytes()?, None)?; + + let blinded_message = BlindedMessage { + amount, + b: blinded, + keyset_id, + }; + + output.push(PreMint { + secret, + blinded_message, + r: r.into(), + amount, + }); + } + + Ok(PreMintSecrets { secrets: output }) + } + pub fn iter(&self) -> impl Iterator { self.secrets.iter() } @@ -322,6 +331,28 @@ pub mod wallet { } } + // Implement Iterator for PreMintSecrets + impl Iterator for PreMintSecrets { + type Item = PreMint; + + fn next(&mut self) -> Option { + // Use the iterator of the vector + self.secrets.pop() + } + } + + impl Ord for PreMintSecrets { + fn cmp(&self, other: &Self) -> Ordering { + self.secrets.cmp(&other.secrets) + } + } + + impl PartialOrd for PreMintSecrets { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Token { pub token: Vec, diff --git a/crates/cashu/src/nuts/nut11.rs b/crates/cashu/src/nuts/nut11.rs index 7e18e1f2..7a71c449 100644 --- a/crates/cashu/src/nuts/nut11.rs +++ b/crates/cashu/src/nuts/nut11.rs @@ -80,13 +80,41 @@ impl PartialOrd for Proof { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct P2PKConditions { + #[serde(skip_serializing_if = "Option::is_none")] pub locktime: Option, pub pubkeys: Vec, - pub refund_keys: Option>, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub refund_keys: Vec, + #[serde(skip_serializing_if = "Option::is_none")] pub num_sigs: Option, pub sig_flag: SigFlag, } +impl P2PKConditions { + pub fn new( + locktime: Option, + pubkeys: Vec, + refund_keys: Vec, + num_sigs: Option, + sig_flag: Option, + ) -> Result { + if let Some(locktime) = locktime { + if locktime.lt(&unix_time()) { + return Err(Error::LocktimeInPast); + } + } + + Ok(Self { + locktime, + pubkeys, + refund_keys, + num_sigs, + sig_flag: sig_flag.unwrap_or_default(), + }) + } +} + impl TryFrom for Secret { type Error = Error; fn try_from(conditions: P2PKConditions) -> Result { @@ -119,7 +147,7 @@ impl TryFrom for Secret { tags.push(Tag::NSigs(num_sigs).as_vec()); } - if let Some(refund_keys) = refund_keys { + if !refund_keys.is_empty() { tags.push(Tag::Refund(refund_keys).as_vec()) } @@ -136,6 +164,15 @@ impl TryFrom for Secret { } } +impl TryFrom for crate::secret::Secret { + type Error = Error; + fn try_from(conditions: P2PKConditions) -> Result { + let secret: Secret = conditions.try_into()?; + + secret.try_into() + } +} + impl TryFrom for P2PKConditions { type Error = Error; fn try_from(secret: Secret) -> Result { @@ -171,11 +208,11 @@ impl TryFrom for P2PKConditions { let refund_keys = if let Some(tag) = tags.get(&TagKind::Refund) { match tag { - Tag::Refund(keys) => Some(keys.clone()), - _ => None, + Tag::Refund(keys) => keys.clone(), + _ => vec![], } } else { - None + vec![] }; let sig_flag = if let Some(tag) = tags.get(&TagKind::SigFlag) { @@ -246,9 +283,9 @@ impl Proof { if let Some(locktime) = spending_conditions.locktime { // If lock time has passed check if refund witness signature is valid if locktime.lt(&unix_time()) { - if let Some(refund_pubkeys) = &spending_conditions.refund_keys { + if !spending_conditions.refund_keys.is_empty() { for s in &self.witness.signatures { - for v in refund_pubkeys { + for v in &spending_conditions.refund_keys { let sig = Signature::try_from(s.as_bytes()) .map_err(|_| Error::InvalidSignature)?; let v: VerifyingKey = v.clone().try_into()?; @@ -325,10 +362,11 @@ where } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash)] pub enum SigFlag { - SigAll, + #[default] SigInputs, + SigAll, Custom(String), } @@ -506,10 +544,10 @@ mod tests { ) .unwrap(), ], - refund_keys: Some(vec![PublicKey::from_str( + refund_keys: vec![PublicKey::from_str( "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", ) - .unwrap()]), + .unwrap()], num_sigs: Some(2), sig_flag: SigFlag::SigAll, }; @@ -545,7 +583,7 @@ mod tests { let conditions = P2PKConditions { locktime: None, pubkeys: vec![v_key.into()], - refund_keys: None, + refund_keys: vec![], num_sigs: None, sig_flag: SigFlag::SigInputs, };