diff --git a/bindings/cashu-ffi/src/error.rs b/bindings/cashu-ffi/src/error.rs index c545a111..151a3c5d 100644 --- a/bindings/cashu-ffi/src/error.rs +++ b/bindings/cashu-ffi/src/error.rs @@ -56,3 +56,11 @@ impl From for CashuError { } } } + +impl From for CashuError { + fn from(err: cashu::url::Error) -> Self { + Self::Generic { + err: err.to_string(), + } + } +} diff --git a/bindings/cashu-ffi/src/nuts/nut00/mint_proofs.rs b/bindings/cashu-ffi/src/nuts/nut00/mint_proofs.rs index 17ce2ced..53c75b60 100644 --- a/bindings/cashu-ffi/src/nuts/nut00/mint_proofs.rs +++ b/bindings/cashu-ffi/src/nuts/nut00/mint_proofs.rs @@ -1,8 +1,10 @@ -use cashu::nuts::nut00::MintProofs as MintProofsSdk; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; +use cashu::nuts::nut00::MintProofs as MintProofsSdk; +use cashu::url::UncheckedUrl; + use crate::error::Result; use crate::Proof; @@ -19,7 +21,7 @@ impl Deref for MintProofs { impl MintProofs { pub fn new(mint: String, proofs: Vec>) -> Result { - let mint = url::Url::from_str(&mint)?; + let mint = UncheckedUrl::from_str(&mint)?; let proofs = proofs.iter().map(|p| p.as_ref().deref().clone()).collect(); Ok(Self { diff --git a/bindings/cashu-ffi/src/nuts/nut00/token.rs b/bindings/cashu-ffi/src/nuts/nut00/token.rs index 5a712169..fd4538ba 100644 --- a/bindings/cashu-ffi/src/nuts/nut00/token.rs +++ b/bindings/cashu-ffi/src/nuts/nut00/token.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use cashu::nuts::nut00::wallet::Token as TokenSdk; +use cashu::url::UncheckedUrl; use crate::error::Result; use crate::MintProofs; @@ -13,7 +14,7 @@ pub struct Token { impl Token { pub fn new(mint: String, proofs: Vec>, memo: Option) -> Result { - let mint = url::Url::from_str(&mint)?; + let mint = UncheckedUrl::from_str(&mint)?; let proofs = proofs.into_iter().map(|p| p.as_ref().into()).collect(); Ok(Self { inner: TokenSdk::new(mint, proofs, memo)?, diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index 278d99ec..b72bfebb 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/crates/cashu-sdk/src/wallet.rs @@ -134,7 +134,7 @@ impl Wallet { pub fn mint_token(&self, amount: Amount, hash: &str) -> Result { let proofs = self.mint(amount, hash)?; - let token = Token::new(self.client.client.mint_url.clone(), proofs, None); + let token = Token::new(self.client.client.mint_url.clone().into(), proofs, None); Ok(token?) } @@ -243,7 +243,7 @@ impl Wallet { { self.mint_keys.clone() } else { - Client::new(token.mint.as_str())?.get_keys()? + Client::new(&token.mint.to_string())?.get_keys()? }; // Sum amount of all proofs @@ -520,7 +520,10 @@ impl Wallet { #[cfg(feature = "blocking")] pub fn proofs_to_token(&self, proofs: Proofs, memo: Option) -> Result { - Ok(Token::new(self.client.client.mint_url.clone(), proofs, memo)?.convert_to_string()?) + Ok( + Token::new(self.client.client.mint_url.clone().into(), proofs, memo)? + .convert_to_string()?, + ) } } diff --git a/crates/cashu/src/error.rs b/crates/cashu/src/error.rs index c50e59be..95902f51 100644 --- a/crates/cashu/src/error.rs +++ b/crates/cashu/src/error.rs @@ -102,6 +102,8 @@ pub mod wallet { UnsupportedToken, /// Token Requires proofs ProofsRequired, + /// Url Parse error + UrlParse, /// Custom Error message CustomError(String), } @@ -118,6 +120,7 @@ pub mod wallet { Error::UnsupportedToken => write!(f, "Unsuppported Token"), Error::EllipticError(err) => write!(f, "{}", err), Error::SerdeJsonError(err) => write!(f, "{}", err), + Error::UrlParse => write!(f, "Could not parse url"), Error::ProofsRequired => write!(f, "Token must have at least one proof",), } } @@ -146,6 +149,12 @@ pub mod wallet { Error::Base64Error(err) } } + + impl From for Error { + fn from(_err: crate::url::Error) -> Error { + Error::UrlParse + } + } } #[cfg(feature = "mint")] diff --git a/crates/cashu/src/lib.rs b/crates/cashu/src/lib.rs index ee9c0090..2f3c2dcb 100644 --- a/crates/cashu/src/lib.rs +++ b/crates/cashu/src/lib.rs @@ -6,6 +6,7 @@ pub mod nuts; pub mod secret; pub mod serde_utils; pub mod types; +pub mod url; pub mod utils; pub use amount::Amount; diff --git a/crates/cashu/src/nuts/nut00.rs b/crates/cashu/src/nuts/nut00.rs index a128b1ac..8aa0e3c3 100644 --- a/crates/cashu/src/nuts/nut00.rs +++ b/crates/cashu/src/nuts/nut00.rs @@ -1,10 +1,7 @@ //! Notation and Models // https://github.com/cashubtc/nuts/blob/main/00.md -use url::Url; - -use crate::Amount; -use crate::{secret::Secret, serde_utils::serde_url}; +use crate::{secret::Secret, url::UncheckedUrl, Amount}; use serde::{Deserialize, Serialize}; use super::nut01::PublicKey; @@ -34,6 +31,7 @@ pub mod wallet { use crate::nuts::nut00::Proofs; use crate::nuts::nut01; use crate::secret::Secret; + use crate::url::UncheckedUrl; use crate::Amount; use crate::{dhke::blind_message, utils::split_amount}; @@ -111,7 +109,7 @@ pub mod wallet { impl Token { pub fn new( - mint_url: Url, + mint_url: UncheckedUrl, proofs: Proofs, memo: Option, ) -> Result { @@ -119,8 +117,11 @@ pub mod wallet { return Err(wallet::Error::ProofsRequired); } + // Check Url is valid + let _: Url = (&mint_url).try_into()?; + Ok(Self { - token: vec![MintProofs::new(mint_url, proofs)], + token: vec![MintProofs::new(mint_url.into(), proofs)], memo, }) } @@ -165,14 +166,13 @@ pub mod wallet { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MintProofs { - #[serde(with = "serde_url")] - pub mint: Url, + pub mint: UncheckedUrl, pub proofs: Proofs, } #[cfg(feature = "wallet")] impl MintProofs { - fn new(mint_url: Url, proofs: Proofs) -> Self { + fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self { Self { mint: mint_url, proofs, @@ -250,7 +250,6 @@ pub mod mint { #[cfg(test)] mod tests { use std::str::FromStr; - use url::Url; use super::wallet::*; use super::*; @@ -268,12 +267,15 @@ mod tests { #[test] fn test_token_str_round_trip() { - let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0="; + let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmsgeW91LiJ9"; + let token = Token::from_str(token_str).unwrap(); assert_eq!( token.token[0].mint, - Url::from_str("https://8333.space:3338").unwrap() + UncheckedUrl::from_str("https://8333.space:3338") + .unwrap() + .into() ); assert_eq!( token.token[0].proofs[0].clone().id.unwrap(), diff --git a/crates/cashu/src/nuts/nut01.rs b/crates/cashu/src/nuts/nut01.rs index d07b0b27..63c7f5cc 100644 --- a/crates/cashu/src/nuts/nut01.rs +++ b/crates/cashu/src/nuts/nut01.rs @@ -78,7 +78,6 @@ impl SecretKey { } /// Mint Keys [NUT-01] -// TODO: CHange this to Amount type #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct Keys(BTreeMap); diff --git a/crates/cashu/src/url.rs b/crates/cashu/src/url.rs new file mode 100644 index 00000000..8158046f --- /dev/null +++ b/crates/cashu/src/url.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! Url + +use core::fmt; +use core::str::FromStr; +use serde::{Deserialize, Serialize}; +use url::{ParseError, Url}; + +/// Url Error +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// Url error + Url(ParseError), +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Url(e) => write!(f, "Url: {e}"), + } + } +} + +impl From for Error { + fn from(e: ParseError) -> Self { + Self::Url(e) + } +} + +/// Unchecked Url +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct UncheckedUrl(String); + +impl UncheckedUrl { + /// New unchecked url + pub fn new(url: S) -> Self + where + S: Into, + { + Self(url.into()) + } + + /// Empty unchecked url + pub fn empty() -> Self { + Self(String::new()) + } +} + +impl From for UncheckedUrl +where + S: Into, +{ + fn from(url: S) -> Self { + Self(url.into()) + } +} + +impl FromStr for UncheckedUrl { + type Err = Error; + + fn from_str(url: &str) -> Result { + Ok(Self::from(url)) + } +} + +impl TryFrom for Url { + type Error = Error; + + fn try_from(unchecked_url: UncheckedUrl) -> Result { + Ok(Self::parse(&unchecked_url.0)?) + } +} + +impl TryFrom<&UncheckedUrl> for Url { + type Error = Error; + + fn try_from(unchecked_url: &UncheckedUrl) -> Result { + Ok(Self::parse(unchecked_url.0.as_str())?) + } +} + +impl fmt::Display for UncheckedUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_unchecked_relay_url() { + let relay = "wss://relay.damus.io/"; + let relay_url = Url::from_str(relay).unwrap(); + + let unchecked_relay_url = UncheckedUrl::from(relay_url.clone()); + + assert_eq!(unchecked_relay_url, UncheckedUrl::from(relay)); + + assert_eq!( + Url::try_from(unchecked_relay_url.clone()).unwrap(), + relay_url + ); + + assert_eq!(relay, unchecked_relay_url.to_string()); + } +}