diff --git a/crates/cashu-sdk/src/client/minreq_client.rs b/crates/cashu-sdk/src/client/minreq_client.rs index 2b3ed37a..747d7839 100644 --- a/crates/cashu-sdk/src/client/minreq_client.rs +++ b/crates/cashu-sdk/src/client/minreq_client.rs @@ -14,7 +14,7 @@ use cashu::nuts::{CheckStateRequest, CheckStateResponse}; use cashu::secret::Secret; use cashu::{Amount, Bolt11Invoice}; use serde_json::Value; -use tracing::{error, warn}; +use tracing::warn; use url::Url; use super::join_url; @@ -170,19 +170,18 @@ impl Client for HttpClient { mint_url: Url, split_request: SwapRequest, ) -> Result { - // TODO: Add to endpoint let url = join_url(mint_url, &["v1", "swap"])?; let res = minreq::post(url).with_json(&split_request)?.send()?; + let value = res.json::()?; let response: Result = - serde_json::from_value(res.json::()?.clone()); + serde_json::from_value(value.clone()); - if let Err(err) = &response { - error!("{}", err) + match response { + Ok(res) => Ok(res), + Err(_) => Err(ErrorResponse::from_json(&value.to_string())?.into()), } - - Ok(response?) } /// Spendable check [NUT-07] diff --git a/crates/cashu-sdk/src/mint/mod.rs b/crates/cashu-sdk/src/mint/mod.rs index 75b51da1..96a5df07 100644 --- a/crates/cashu-sdk/src/mint/mod.rs +++ b/crates/cashu-sdk/src/mint/mod.rs @@ -64,10 +64,10 @@ pub enum Error { } impl From for ErrorResponse { - fn from(_err: Error) -> ErrorResponse { + fn from(err: Error) -> ErrorResponse { ErrorResponse { code: 9999, - error: None, + error: Some(err.to_string()), detail: None, } } diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index 66efc793..a7047733 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -6,9 +6,10 @@ use bip39::Mnemonic; use cashu::dhke::{construct_proofs, unblind_message}; #[cfg(feature = "nut07")] use cashu::nuts::nut07::ProofState; +use cashu::nuts::nut11::SigningKey; use cashu::nuts::{ - BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PreSwap, Proof, - Proofs, SwapRequest, Token, + BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, P2PKConditions, PreMintSecrets, + PreSwap, Proof, Proofs, SwapRequest, Token, }; #[cfg(feature = "nut07")] use cashu::secret::Secret; @@ -363,7 +364,7 @@ impl Wallet { let swap_response = self .client - .post_swap(token.mint.clone().try_into()?, pre_swap.split_request) + .post_swap(token.mint.clone().try_into()?, pre_swap.swap_request) .await?; // Proof to keep @@ -393,9 +394,6 @@ impl Wallet { amount: Option, proofs: Proofs, ) -> Result { - // 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(); let pre_mint_secrets = if let Some(amount) = amount { @@ -415,11 +413,11 @@ impl Wallet { PreMintSecrets::random(active_keyset_id, value)? }; - let split_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages()); + let swap_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages()); Ok(PreSwap { pre_mint_secrets, - split_request, + swap_request, }) } @@ -471,7 +469,7 @@ impl Wallet { let swap_response = self .client - .post_swap(mint_url.clone().try_into()?, pre_swap.split_request) + .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) .await?; let mut keep_proofs = Proofs::new(); @@ -553,7 +551,7 @@ impl Wallet { } // Select proofs - async fn select_proofs( + pub async fn select_proofs( &self, mint_url: UncheckedUrl, unit: &CurrencyUnit, @@ -676,6 +674,118 @@ impl Wallet { Ok(melted) } + /// Create P2PK locked proofs + /// Uses a swap to swap proofs for locked p2pk conditions + pub async fn create_p2pk_proofs( + &mut self, + mint_url: &UncheckedUrl, + unit: &CurrencyUnit, + input_proofs: Proofs, + conditions: P2PKConditions, + ) -> Result { + let amount = input_proofs.iter().map(|p| p.amount).sum(); + let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?.unwrap(); + + let pre_mint_secrets = + PreMintSecrets::with_p2pk_conditions(active_keyset_id, amount, conditions)?; + let swap_request = + SwapRequest::new(input_proofs.clone(), pre_mint_secrets.blinded_messages()); + + let pre_swap = PreSwap { + pre_mint_secrets, + swap_request, + }; + + let swap_response = self + .client + .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) + .await?; + + let post_swap_proofs = construct_proofs( + swap_response.signatures, + pre_swap.pre_mint_secrets.rs(), + pre_swap.pre_mint_secrets.secrets(), + &self.active_keys(mint_url, unit).await?.unwrap(), + )?; + + let mut send_proofs = vec![]; + let mut change_proofs = vec![]; + + for proof in post_swap_proofs { + let conditions: Result = (&proof.secret).try_into(); + println!("{:?}", conditions); + if conditions.is_ok() { + send_proofs.push(proof); + } else { + change_proofs.push(proof); + } + } + + self.localstore + .remove_proofs(mint_url.clone(), &input_proofs) + .await?; + + self.localstore + .add_pending_proofs(mint_url.clone(), input_proofs) + .await?; + self.localstore + .add_pending_proofs(mint_url.clone(), send_proofs.clone()) + .await?; + self.localstore + .add_proofs(mint_url.clone(), change_proofs.clone()) + .await?; + + Ok(send_proofs) + } + + pub async fn claim_p2pk_locked_proof( + &mut self, + mint_url: &UncheckedUrl, + unit: &CurrencyUnit, + signing_key: SigningKey, + proofs: Proofs, + ) -> Result<(), Error> { + let active_keyset_id = self.active_mint_keyset(&mint_url, &unit).await?; + + let keys = self.localstore.get_keys(&active_keyset_id.unwrap()).await?; + + let mut signed_proofs: Proofs = Vec::with_capacity(proofs.len()); + + // Sum amount of all proofs + let amount: Amount = proofs.iter().map(|p| p.amount).sum(); + + for p in proofs.clone() { + let mut p = p; + p.sign_p2pk_proof(signing_key.clone()).unwrap(); + signed_proofs.push(p); + } + + let pre_swap = self + .create_swap(mint_url, &unit, Some(amount), signed_proofs) + .await?; + + let swap_response = self + .client + .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request) + .await?; + + // Proof to keep + let p = construct_proofs( + swap_response.signatures, + pre_swap.pre_mint_secrets.rs(), + pre_swap.pre_mint_secrets.secrets(), + &keys.unwrap(), + )?; + + self.localstore + .remove_proofs(mint_url.clone(), &proofs) + .await?; + + self.localstore.add_proofs(mint_url.clone(), p).await?; + + Ok(()) + } + pub fn proofs_to_token( &self, mint_url: UncheckedUrl, diff --git a/crates/cashu/src/dhke.rs b/crates/cashu/src/dhke.rs index f6f67fc3..1a9a8b27 100644 --- a/crates/cashu/src/dhke.rs +++ b/crates/cashu/src/dhke.rs @@ -229,7 +229,7 @@ mod tests { assert_eq!(sec, r.into()); assert_eq!( - b.to_hex(), + b.to_string(), PublicKey::from( k256::PublicKey::from_sec1_bytes( &hex::decode( @@ -239,7 +239,7 @@ mod tests { ) .unwrap() ) - .to_hex() + .to_string() ); let message = "f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60"; @@ -254,7 +254,7 @@ mod tests { assert_eq!(sec, r.into()); assert_eq!( - b.to_hex(), + b.to_string(), PublicKey::from( k256::PublicKey::from_sec1_bytes( &hex::decode( @@ -264,7 +264,7 @@ mod tests { ) .unwrap() ) - .to_hex() + .to_string() ); } diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index cd4ddf48..c267807f 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -40,6 +40,6 @@ pub use nut08::{MeltBolt11Request, MeltBolt11Response}; #[cfg(feature = "nut10")] pub use nut10::{Kind, Secret as Nut10Secret, SecretData}; #[cfg(feature = "nut11")] -pub use nut11::{P2PKConditions, Proof, SigFlag, Signatures}; +pub use nut11::{P2PKConditions, Proof, SigFlag, Signatures, SigningKey, VerifyingKey}; pub type Proofs = Vec; diff --git a/crates/cashu/src/nuts/nut01.rs b/crates/cashu/src/nuts/nut01.rs index 25cfad9a..050e682b 100644 --- a/crates/cashu/src/nuts/nut01.rs +++ b/crates/cashu/src/nuts/nut01.rs @@ -65,16 +65,6 @@ impl From for PublicKey { } impl PublicKey { - pub fn from_hex(hex: String) -> Result { - let hex = hex::decode(hex)?; - Ok(PublicKey(k256::PublicKey::from_sec1_bytes(&hex)?)) - } - - pub fn to_hex(&self) -> String { - let bytes = self.0.to_sec1_bytes(); - hex::encode(bytes) - } - pub fn to_bytes(&self) -> Box<[u8]> { self.0.to_sec1_bytes() } @@ -91,7 +81,8 @@ impl FromStr for PublicKey { impl std::fmt::Display for PublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.to_hex()) + let bytes = self.0.to_sec1_bytes(); + f.write_str(&hex::encode(bytes)) } } @@ -280,9 +271,12 @@ mod tests { #[test] fn pubkey() { let pubkey_str = "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4"; + let pubkey = PublicKey::from_str(pubkey_str).unwrap(); + assert_eq!(pubkey_str, pubkey.to_string()); + /* + let pubkey_str = "04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5"; let pubkey = PublicKey::from_hex(pubkey_str.to_string()).unwrap(); - - assert_eq!(pubkey_str, pubkey.to_hex()) + assert_eq!(pubkey_str, pubkey.to_hex())*/ } #[test] diff --git a/crates/cashu/src/nuts/nut03.rs b/crates/cashu/src/nuts/nut03.rs index 1bf5af8e..6f235e87 100644 --- a/crates/cashu/src/nuts/nut03.rs +++ b/crates/cashu/src/nuts/nut03.rs @@ -14,7 +14,7 @@ pub use crate::Bolt11Invoice; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct PreSwap { pub pre_mint_secrets: PreMintSecrets, - pub split_request: SwapRequest, + pub swap_request: SwapRequest, } /// Split Request [NUT-06] diff --git a/crates/cashu/src/nuts/nut11.rs b/crates/cashu/src/nuts/nut11.rs index 7a71c449..435d3e41 100644 --- a/crates/cashu/src/nuts/nut11.rs +++ b/crates/cashu/src/nuts/nut11.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use bitcoin::hashes::{sha256, Hash}; use k256::schnorr::signature::{Signer, Verifier}; -use k256::schnorr::{Signature, SigningKey, VerifyingKey}; +use k256::schnorr::Signature; use serde::de::Error as DeserializerError; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -16,17 +16,22 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::nut01::PublicKey; use super::nut02::Id; use super::nut10::{Secret, SecretData}; +use super::SecretKey; use crate::error::Error; use crate::utils::unix_time; use crate::Amount; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Signatures { + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] signatures: Vec, } -fn no_signatures(signatures: &Signatures) -> bool { - signatures.signatures.is_empty() +impl Signatures { + pub fn is_empty(&self) -> bool { + self.signatures.is_empty() + } } /// Proofs [NUT-11] @@ -44,7 +49,7 @@ pub struct Proof { pub keyset_id: Id, /// Witness #[serde(default)] - #[serde(skip_serializing_if = "no_signatures")] + #[serde(skip_serializing_if = "Signatures::is_empty")] pub witness: Signatures, } @@ -82,10 +87,10 @@ impl PartialOrd for Proof { pub struct P2PKConditions { #[serde(skip_serializing_if = "Option::is_none")] pub locktime: Option, - pub pubkeys: Vec, + pub pubkeys: Vec, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub refund_keys: Vec, + pub refund_keys: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub num_sigs: Option, pub sig_flag: SigFlag, @@ -94,8 +99,8 @@ pub struct P2PKConditions { impl P2PKConditions { pub fn new( locktime: Option, - pubkeys: Vec, - refund_keys: Vec, + pubkeys: Vec, + refund_keys: Vec, num_sigs: Option, sig_flag: Option, ) -> Result { @@ -131,7 +136,7 @@ impl TryFrom for Secret { return Err(Error::Amount); } - let data = pubkeys[0].to_hex(); + let data = pubkeys[0].to_string(); let mut tags = vec![]; @@ -187,14 +192,14 @@ impl TryFrom for P2PKConditions { }) .collect(); - let mut pubkeys: Vec = vec![]; + let mut pubkeys: Vec = vec![]; if let Some(Tag::PubKeys(keys)) = tags.get(&TagKind::Pubkeys) { let mut keys = keys.clone(); pubkeys.append(&mut keys); } - let data_pubkey = PublicKey::from_hex(secret.secret_data.data)?; + let data_pubkey = VerifyingKey::from_str(&secret.secret_data.data)?; pubkeys.push(data_pubkey); let locktime = if let Some(tag) = tags.get(&TagKind::Locktime) { @@ -258,20 +263,15 @@ impl Proof { for signature in &self.witness.signatures { let mut pubkeys = spending_conditions.pubkeys.clone(); - let data_key = PublicKey::from_str(&secret.secret_data.data).unwrap(); + let data_key = VerifyingKey::from_str(&secret.secret_data.data).unwrap(); pubkeys.push(data_key); for v in &spending_conditions.pubkeys { let sig = Signature::try_from(hex::decode(signature).unwrap().as_slice()).unwrap(); - let verifying_key: VerifyingKey = v.try_into()?; - - if verifying_key.verify(&msg.to_byte_array(), &sig).is_ok() { + if v.verify(&msg.to_byte_array(), &sig).is_ok() { valid_sigs += 1; } else { - println!( - "{:?}", - verifying_key.verify(&msg.to_byte_array(), &sig).unwrap() - ); + println!("{:?}", v.verify(&msg.to_byte_array(), &sig).unwrap()); } } } @@ -288,7 +288,6 @@ impl Proof { 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()?; // As long as there is one valid refund signature it can be spent if v.verify(&msg.to_byte_array(), &sig).is_ok() { @@ -398,8 +397,8 @@ pub enum Tag { SigFlag(SigFlag), NSigs(u64), LockTime(u64), - Refund(Vec), - PubKeys(Vec), + Refund(Vec), + PubKeys(Vec), } impl Tag { @@ -445,7 +444,7 @@ where let pubkeys = tag .iter() .skip(1) - .flat_map(|p| PublicKey::from_hex(p.as_ref().to_string())) + .flat_map(|p| VerifyingKey::from_str(p.as_ref())) .collect(); Ok(Self::Refund(pubkeys)) @@ -454,7 +453,7 @@ where let pubkeys = tag .iter() .skip(1) - .flat_map(|p| PublicKey::from_hex(p.as_ref().to_string())) + .flat_map(|p| VerifyingKey::from_str(p.as_ref())) .collect(); Ok(Self::PubKeys(pubkeys)) @@ -477,7 +476,7 @@ impl From for Vec { let mut tag = vec![TagKind::Pubkeys.to_string()]; for pubkey in pubkeys { - tag.push(pubkey.to_hex()) + tag.push(pubkey.to_string()) } tag } @@ -485,7 +484,7 @@ impl From for Vec { let mut tag = vec![TagKind::Refund.to_string()]; for pubkey in pubkeys { - tag.push(pubkey.to_hex()) + tag.push(pubkey.to_string()) } tag } @@ -518,33 +517,174 @@ impl<'de> Deserialize<'de> for Tag { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct VerifyingKey(k256::schnorr::VerifyingKey); + +impl VerifyingKey { + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(VerifyingKey( + k256::schnorr::VerifyingKey::from_bytes(bytes).unwrap(), + )) + } + + pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { + Ok(self.0.verify(msg, signature).unwrap()) + } +} + +impl FromStr for VerifyingKey { + type Err = Error; + + fn from_str(hex: &str) -> Result { + let bytes = hex::decode(hex)?; + + let bytes = if bytes.len().eq(&33) { + bytes.iter().skip(1).cloned().collect() + } else { + bytes.to_vec() + }; + + Ok(VerifyingKey( + k256::schnorr::VerifyingKey::from_bytes(&bytes).map_err(|_| Error::Key)?, + )) + } +} + +impl std::fmt::Display for VerifyingKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = self.0.to_bytes(); + f.write_str(&hex::encode(bytes)) + } +} + +impl From for k256::schnorr::VerifyingKey { + fn from(value: VerifyingKey) -> k256::schnorr::VerifyingKey { + value.0 + } +} + +impl From<&VerifyingKey> for k256::schnorr::VerifyingKey { + fn from(value: &VerifyingKey) -> k256::schnorr::VerifyingKey { + value.0 + } +} + +impl From for VerifyingKey { + fn from(value: k256::schnorr::VerifyingKey) -> VerifyingKey { + VerifyingKey(value) + } +} + +impl TryFrom for VerifyingKey { + type Error = Error; + fn try_from(value: PublicKey) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&PublicKey> for VerifyingKey { + type Error = Error; + fn try_from(value: &PublicKey) -> Result { + let bytes = value.to_bytes(); + + let bytes = if bytes.len().eq(&33) { + bytes.iter().skip(1).cloned().collect() + } else { + bytes.to_vec() + }; + + VerifyingKey::from_bytes(&bytes).map_err(|_| Error::Key) + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SigningKey(k256::schnorr::SigningKey); + +impl From for k256::schnorr::SigningKey { + fn from(value: SigningKey) -> k256::schnorr::SigningKey { + value.0 + } +} + +impl From for SigningKey { + fn from(value: k256::schnorr::SigningKey) -> Self { + Self(value) + } +} + +impl From for SigningKey { + fn from(value: SecretKey) -> SigningKey { + value.into() + } +} + +impl SigningKey { + pub fn public_key(&self) -> VerifyingKey { + self.0.verifying_key().clone().into() + } + + pub fn sign(&self, msg: &[u8]) -> Signature { + self.0.sign(msg) + } + + pub fn verifying_key(&self) -> VerifyingKey { + VerifyingKey(self.0.verifying_key().clone()) + } +} + +impl FromStr for SigningKey { + type Err = Error; + + fn from_str(hex: &str) -> Result { + let bytes = hex::decode(hex)?; + + let bytes = if bytes.len().eq(&33) { + bytes.iter().skip(1).cloned().collect() + } else { + bytes.to_vec() + }; + + Ok(SigningKey( + k256::schnorr::SigningKey::from_bytes(&bytes).map_err(|_| Error::Key)?, + )) + } +} + +impl std::fmt::Display for SigningKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = self.0.to_bytes(); + + f.write_str(&hex::encode(bytes)) + } +} #[cfg(test)] mod tests { use std::str::FromStr; use super::*; - use crate::nuts::SecretKey; #[test] fn test_secret_ser() { let conditions = P2PKConditions { locktime: Some(99999), pubkeys: vec![ - PublicKey::from_str( + VerifyingKey::from_str( "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", ) .unwrap(), - PublicKey::from_str( + VerifyingKey::from_str( "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", ) .unwrap(), - PublicKey::from_str( + VerifyingKey::from_str( "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54", ) .unwrap(), ], - refund_keys: vec![PublicKey::from_str( + refund_keys: vec![VerifyingKey::from_str( "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", ) .unwrap()], @@ -572,13 +712,12 @@ mod tests { #[test] fn sign_proof() { - let secret_key = - SecretKey::from_hex("04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5") - .unwrap(); + let secret_key = SigningKey::from_str( + "04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5", + ) + .unwrap(); - let pubkey: PublicKey = secret_key.public_key(); - - let v_key: VerifyingKey = pubkey.clone().try_into().unwrap(); + let v_key: VerifyingKey = secret_key.verifying_key(); let conditions = P2PKConditions { locktime: None,