Skip to content

Commit

Permalink
crawB Binary Token Serialization (#507)
Browse files Browse the repository at this point in the history
  • Loading branch information
lollerfirst authored Dec 19, 2024
1 parent 0e12314 commit 8055c0c
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 1 deletion.
5 changes: 4 additions & 1 deletion crates/cdk/src/nuts/nut00/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::io::Error>),
/// Ciborium serialization error
#[error(transparent)]
CiboriumSerError(#[from] ciborium::ser::Error<std::io::Error>),
/// Amount Error
#[error(transparent)]
Amount(#[from] crate::amount::Error),
Expand Down
76 changes: 76 additions & 0 deletions crates/cdk/src/nuts/nut00/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ impl Token {

v3_token.to_string()
}

/// Serialize the token to raw binary
pub fn to_raw_bytes(&self) -> Result<Vec<u8>, Error> {
match self {
Self::TokenV3(_) => Err(Error::UnsupportedToken),
Self::TokenV4(token) => token.to_raw_bytes(),
}
}
}

impl FromStr for Token {
Expand Down Expand Up @@ -152,6 +160,26 @@ impl FromStr for Token {
}
}

impl TryFrom<&Vec<u8>> for Token {
type Error = Error;

fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
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 {
Expand Down Expand Up @@ -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<Vec<u8>, 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 {
Expand All @@ -355,6 +392,25 @@ impl FromStr for TokenV4 {
}
}

impl TryFrom<&Vec<u8>> for TokenV4 {
type Error = Error;

fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
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<TokenV3> for TokenV4 {
type Error = Error;
fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
Expand Down Expand Up @@ -434,6 +490,7 @@ mod tests {

use super::*;
use crate::mint_url::MintUrl;
use crate::util::hex;

#[test]
fn test_token_padding() {
Expand Down Expand Up @@ -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);
}
}
43 changes: 43 additions & 0 deletions crates/cdk/src/wallet/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
amount_split_target: SplitTarget,
p2pk_signing_keys: &[SecretKey],
preimages: &[String],
) -> Result<Amount, Error> {
let token_str = Token::try_from(binary_token)?.to_string();
self.receive(
token_str.as_str(),
amount_split_target,
p2pk_signing_keys,
preimages,
)
.await
}
}

0 comments on commit 8055c0c

Please sign in to comment.