From 8055c0ced1d93f02c84b24dfa1275a0a2c074ed0 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:27:05 +0100 Subject: [PATCH] `crawB` Binary Token Serialization (#507) --- crates/cdk/src/nuts/nut00/mod.rs | 5 +- crates/cdk/src/nuts/nut00/token.rs | 76 ++++++++++++++++++++++++++++++ crates/cdk/src/wallet/receive.rs | 43 +++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 929b0c05..ae459ee3 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -75,9 +75,12 @@ pub enum Error { /// Base64 error #[error(transparent)] Base64Error(#[from] bitcoin::base64::DecodeError), - /// Ciborium error + /// Ciborium deserialization error #[error(transparent)] CiboriumError(#[from] ciborium::de::Error), + /// Ciborium serialization error + #[error(transparent)] + CiboriumSerError(#[from] ciborium::ser::Error), /// Amount Error #[error(transparent)] Amount(#[from] crate::amount::Error), diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index dccd695b..724de0e7 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -122,6 +122,14 @@ impl Token { v3_token.to_string() } + + /// Serialize the token to raw binary + pub fn to_raw_bytes(&self) -> Result, Error> { + match self { + Self::TokenV3(_) => Err(Error::UnsupportedToken), + Self::TokenV4(token) => token.to_raw_bytes(), + } + } } impl FromStr for Token { @@ -152,6 +160,26 @@ impl FromStr for Token { } } +impl TryFrom<&Vec> for Token { + type Error = Error; + + fn try_from(bytes: &Vec) -> Result { + if bytes.len() < 5 { + return Err(Error::UnsupportedToken); + } + + let prefix = String::from_utf8(bytes[..5].to_vec())?; + + match prefix.as_str() { + "crawB" => { + let token: TokenV4 = ciborium::from_reader(&bytes[5..])?; + Ok(Token::TokenV4(token)) + } + _ => Err(Error::UnsupportedToken), + } + } +} + /// Token V3 Token #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TokenV3Token { @@ -329,6 +357,15 @@ impl TokenV4 { pub fn unit(&self) -> &CurrencyUnit { &self.unit } + + /// Serialize the token to raw binary + pub fn to_raw_bytes(&self) -> Result, Error> { + let mut prefix = b"crawB".to_vec(); + let mut data = Vec::new(); + ciborium::into_writer(self, &mut data).map_err(Error::CiboriumSerError)?; + prefix.extend(data); + Ok(prefix) + } } impl fmt::Display for TokenV4 { @@ -355,6 +392,25 @@ impl FromStr for TokenV4 { } } +impl TryFrom<&Vec> for TokenV4 { + type Error = Error; + + fn try_from(bytes: &Vec) -> Result { + if bytes.len() < 5 { + return Err(Error::UnsupportedToken); + } + + let prefix = String::from_utf8(bytes[..5].to_vec())?; + + if prefix.as_str() == "crawB" { + let token: TokenV4 = ciborium::from_reader(&bytes[5..])?; + Ok(token) + } else { + Err(Error::UnsupportedToken) + } + } +} + impl TryFrom for TokenV4 { type Error = Error; fn try_from(token: TokenV3) -> Result { @@ -434,6 +490,7 @@ mod tests { use super::*; use crate::mint_url::MintUrl; + use crate::util::hex; #[test] fn test_token_padding() { @@ -554,4 +611,23 @@ mod tests { assert!(correct_token.is_ok()); } + + #[test] + fn test_token_v4_raw_roundtrip() { + let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap(); + let token = TokenV4::try_from(&token_raw).expect("Token deserialization error"); + let token_raw_ = token.to_raw_bytes().expect("Token serialization error"); + let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error"); + assert!(token_ == token) + } + + #[test] + fn test_token_generic_raw_roundtrip() { + let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap(); + let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error"); + let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error"); + let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error"); + let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error"); + assert!(tokenv4_bytes_ == tokenv4_bytes); + } } diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 1a1cb75a..aed875a3 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -213,4 +213,47 @@ impl Wallet { Ok(amount) } + + /// Receive + /// # Synopsis + /// ```rust, no_run + /// use std::sync::Arc; + /// + /// use cdk::amount::SplitTarget; + /// use cdk::cdk_database::WalletMemoryDatabase; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use cdk::util::hex; + /// use rand::Rng; + /// + /// #[tokio::main] + /// async fn main() -> anyhow::Result<()> { + /// let seed = rand::thread_rng().gen::<[u8; 32]>(); + /// let mint_url = "https://testnut.cashu.space"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = WalletMemoryDatabase::default(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap(); + /// let amount_receive = wallet.receive_raw(&token_raw, SplitTarget::default(), &[], &[]).await?; + /// Ok(()) + /// } + /// ``` + #[instrument(skip_all)] + pub async fn receive_raw( + &self, + binary_token: &Vec, + amount_split_target: SplitTarget, + p2pk_signing_keys: &[SecretKey], + preimages: &[String], + ) -> Result { + let token_str = Token::try_from(binary_token)?.to_string(); + self.receive( + token_str.as_str(), + amount_split_target, + p2pk_signing_keys, + preimages, + ) + .await + } }