diff --git a/src/amount.rs b/src/amount.rs index cb9bc343..07c7b3c7 100644 --- a/src/amount.rs +++ b/src/amount.rs @@ -13,7 +13,6 @@ impl Amount { pub fn split(&self) -> Vec { let sats = self.0.to_sat(); (0_u64..64) - .into_iter() .rev() .filter_map(|bit| { let part = 1 << bit; diff --git a/src/cashu_wallet.rs b/src/cashu_wallet.rs index aae26121..249e840a 100644 --- a/src/cashu_wallet.rs +++ b/src/cashu_wallet.rs @@ -1,18 +1,14 @@ //! Cashu Wallet use std::str::FromStr; +use crate::nuts::nut00::{mint, BlindedMessages, Proofs, Token}; +use crate::nuts::nut01::Keys; +use crate::nuts::nut03::RequestMintResponse; +use crate::nuts::nut06::{SplitPayload, SplitRequest}; +use crate::nuts::nut08::MeltResponse; +use crate::types::{ProofsStatus, SendProofs}; pub use crate::Invoice; -use crate::{ - client::Client, - dhke::construct_proofs, - error::Error, - keyset::Keys, - mint, - types::{ - BlindedMessages, Melted, Proofs, ProofsStatus, RequestMintResponse, SendProofs, - SplitPayload, SplitRequest, Token, - }, -}; +use crate::{client::Client, dhke::construct_proofs, error::Error}; use crate::amount::Amount; @@ -226,28 +222,14 @@ impl CashuWallet { invoice: Invoice, proofs: Proofs, fee_reserve: Amount, - ) -> Result { + ) -> Result { let change = BlindedMessages::blank(fee_reserve)?; let melt_response = self .client .melt(proofs, invoice, Some(change.blinded_messages)) .await?; - let change = match melt_response.change { - Some(promises) => Some(construct_proofs( - promises, - change.rs, - change.secrets, - &self.mint_keys, - )?), - None => None, - }; - - Ok(Melted { - paid: melt_response.paid, - preimage: melt_response.preimage, - change, - }) + Ok(melt_response) } pub fn proofs_to_token(&self, proofs: Proofs, memo: Option) -> Result { diff --git a/src/client.rs b/src/client.rs index d03cc3bf..678c42dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,22 +1,23 @@ //! Client to connet to mint use std::fmt; +use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; use crate::amount::Amount; +use crate::nuts::nut00::{BlindedMessage, BlindedMessages, Proof}; +use crate::nuts::nut01::Keys; +use crate::nuts::nut03::RequestMintResponse; +use crate::nuts::nut04::{MintRequest, PostMintResponse}; +use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse}; +use crate::nuts::nut06::{SplitRequest, SplitResponse}; +use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse}; +use crate::nuts::nut08::{MeltRequest, MeltResponse}; +use crate::nuts::nut09::MintInfo; +use crate::nuts::*; +use crate::utils; pub use crate::Invoice; -use crate::{ - keyset::{self, Keys}, - mint, - types::{ - BlindedMessage, BlindedMessages, CheckFeesRequest, CheckFeesResponse, - CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse, MintInfo, - MintRequest, PostMintResponse, Proof, RequestMintResponse, SplitRequest, SplitResponse, - }, - utils, -}; -use serde::{Deserialize, Serialize}; #[derive(Debug)] pub enum Error { @@ -140,11 +141,11 @@ impl Client { } /// Get Keysets [NUT-02] - pub async fn get_keysets(&self) -> Result { + pub async fn get_keysets(&self) -> Result { let url = self.mint_url.join("keysets")?; let res = minreq::get(url).send()?.json::()?; - let response: Result = + let response: Result = serde_json::from_value(res.clone()); match response { @@ -268,7 +269,7 @@ impl Client { /// Spendable check [NUT-07] pub async fn check_spendable( &self, - proofs: &Vec, + proofs: &Vec, ) -> Result { let url = self.mint_url.join("check")?; let request = CheckSpendableRequest { diff --git a/src/dhke.rs b/src/dhke.rs index 6626b156..386a752d 100644 --- a/src/dhke.rs +++ b/src/dhke.rs @@ -7,9 +7,12 @@ use bitcoin_hashes::Hash; use k256::{ProjectivePoint, Scalar, SecretKey}; use crate::error::Error; -use crate::keyset; -use crate::keyset::{Keys, PublicKey}; -use crate::types::{Promise, Proof, Proofs}; +use crate::nuts::nut00::BlindedSignature; +use crate::nuts::nut00::Proof; +use crate::nuts::nut00::Proofs; +use crate::nuts::nut01::Keys; +use crate::nuts::nut01::PublicKey; +use crate::nuts::*; fn hash_to_curve(message: &[u8]) -> k256::PublicKey { let mut msg_to_hash = message.to_vec(); @@ -66,8 +69,8 @@ pub fn unblind_message( /// Construct Proof pub fn construct_proofs( - promises: Vec, - rs: Vec, + promises: Vec, + rs: Vec, secrets: Vec, keys: &Keys, ) -> Result { diff --git a/src/lib.rs b/src/lib.rs index a1842920..0cf37ed6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,8 @@ pub mod cashu_wallet; pub mod client; pub mod dhke; pub mod error; -pub mod keyset; pub mod mint; +pub mod nuts; pub mod serde_utils; pub mod types; pub mod utils; diff --git a/src/mint.rs b/src/mint.rs index 9299a39a..bf089e17 100644 --- a/src/mint.rs +++ b/src/mint.rs @@ -1,27 +1,23 @@ use std::collections::{HashMap, HashSet}; -use serde::{Deserialize, Serialize}; - +use crate::dhke::sign_message; use crate::dhke::verify_message; use crate::error::Error; -use crate::types::{ - self, BlindedMessage, CheckSpendableRequest, CheckSpendableResponse, MeltRequest, MeltResponse, - PostMintResponse, Promise, SplitRequest, SplitResponse, -}; +use crate::nuts::nut00::BlindedMessage; +use crate::nuts::nut00::BlindedSignature; +use crate::nuts::nut00::Proof; +use crate::nuts::nut06::SplitRequest; +use crate::nuts::nut06::SplitResponse; +use crate::nuts::nut07::CheckSpendableRequest; +use crate::nuts::nut07::CheckSpendableResponse; +use crate::nuts::nut08::MeltRequest; +use crate::nuts::nut08::MeltResponse; +use crate::nuts::*; use crate::Amount; -use crate::{ - dhke::sign_message, - keyset::{ - self, - mint::{self, KeySet}, - PublicKey, - }, - types::MintRequest, -}; pub struct Mint { - pub active_keyset: KeySet, - pub inactive_keysets: HashMap, + pub active_keyset: nut02::mint::KeySet, + pub inactive_keysets: HashMap, pub spent_secrets: HashSet, } @@ -29,12 +25,12 @@ impl Mint { pub fn new( secret: &str, derivation_path: &str, - inactive_keysets: HashMap, + inactive_keysets: HashMap, spent_secrets: HashSet, max_order: u8, ) -> Self { Self { - active_keyset: keyset::mint::KeySet::generate(secret, derivation_path, max_order), + active_keyset: nut02::mint::KeySet::generate(secret, derivation_path, max_order), inactive_keysets, spent_secrets, } @@ -42,23 +38,23 @@ impl Mint { /// Retrieve the public keys of the active keyset for distribution to /// wallet clients - pub fn active_keyset_pubkeys(&self) -> keyset::KeySet { - keyset::KeySet::from(self.active_keyset.clone()) + pub fn active_keyset_pubkeys(&self) -> nut02::KeySet { + nut02::KeySet::from(self.active_keyset.clone()) } /// Return a list of all supported keysets - pub fn keysets(&self) -> keyset::Response { + pub fn keysets(&self) -> nut02::Response { let mut keysets: HashSet<_> = self.inactive_keysets.keys().cloned().collect(); keysets.insert(self.active_keyset.id.clone()); - keyset::Response { keysets } + nut02::Response { keysets } } - pub fn active_keyset(&self) -> keyset::mint::KeySet { + pub fn active_keyset(&self) -> nut02::mint::KeySet { self.active_keyset.clone() } - pub fn keyset(&self, id: &str) -> Option { - if &self.active_keyset.id == id { + pub fn keyset(&self, id: &str) -> Option { + if self.active_keyset.id == id { return Some(self.active_keyset.clone().into()); } @@ -67,20 +63,20 @@ impl Mint { pub fn process_mint_request( &mut self, - mint_request: MintRequest, - ) -> Result { + mint_request: nut04::MintRequest, + ) -> Result { let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len()); for blinded_message in mint_request.outputs { blind_signatures.push(self.blind_sign(&blinded_message)?); } - Ok(PostMintResponse { + Ok(nut04::PostMintResponse { promises: blind_signatures, }) } - fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result { + fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result { let BlindedMessage { amount, b } = blinded_message; let Some(key_pair) = self.active_keyset.keys.0.get(&amount.to_sat()) else { @@ -90,8 +86,8 @@ impl Mint { let c = sign_message(key_pair.secret_key.clone(), b.clone().into())?; - Ok(Promise { - amount: amount.clone(), + Ok(BlindedSignature { + amount: *amount, c: c.into(), id: self.active_keyset.id.clone(), }) @@ -111,7 +107,7 @@ impl Mint { // in the outputs (blind messages). As we loop, take from those sets, // target amount first. for output in outputs { - let signed = self.blind_sign(&output)?; + let signed = self.blind_sign(output)?; // Accumulate outputs into the target (send) list if target_total + signed.amount <= amount { @@ -172,7 +168,7 @@ impl Mint { Ok(split_response) } - fn verify_proof(&self, proof: &types::Proof) -> Result { + fn verify_proof(&self, proof: &Proof) -> Result { if self.spent_secrets.contains(&proof.secret) { return Err(Error::TokenSpent); } @@ -223,7 +219,7 @@ impl Mint { let mut secrets = Vec::with_capacity(melt_request.proofs.len()); for proof in &melt_request.proofs { - secrets.push(self.verify_proof(&proof)?); + secrets.push(self.verify_proof(proof)?); } Ok(()) @@ -248,7 +244,7 @@ impl Mint { for (i, amount) in amounts.iter().enumerate() { let mut message = outputs[i].clone(); - message.amount = amount.clone(); + message.amount = *amount; let signature = self.blind_sign(&message)?; change.push(signature) @@ -262,24 +258,3 @@ impl Mint { }) } } - -/// Proofs [NUT-00] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Proof { - /// Amount in satoshi - pub amount: Option, - /// Secret message - // #[serde(with = "crate::serde_utils::bytes_base64")] - pub secret: String, - /// Unblinded signature - #[serde(rename = "C")] - pub c: Option, - /// `Keyset id` - pub id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// P2SHScript that specifies the spending condition for this Proof - pub script: Option, -} - -/// List of proofs -pub type Proofs = Vec; diff --git a/src/nuts/mod.rs b/src/nuts/mod.rs new file mode 100644 index 00000000..72608a54 --- /dev/null +++ b/src/nuts/mod.rs @@ -0,0 +1,10 @@ +pub mod nut00; +pub mod nut01; +pub mod nut02; +pub mod nut03; +pub mod nut04; +pub mod nut05; +pub mod nut06; +pub mod nut07; +pub mod nut08; +pub mod nut09; diff --git a/src/nuts/nut00.rs b/src/nuts/nut00.rs new file mode 100644 index 00000000..a1dda3d3 --- /dev/null +++ b/src/nuts/nut00.rs @@ -0,0 +1,256 @@ +//! Notation and Models +// https://github.com/cashubtc/nuts/blob/main/00.md + +use std::str::FromStr; + +use base64::{engine::general_purpose, Engine as _}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::utils::generate_secret; +use crate::Amount; +use crate::{dhke::blind_message, error::Error, serde_utils::serde_url, utils::split_amount}; + +use super::nut01::{self, PublicKey}; + +/// Blinded Message [NUT-00] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlindedMessage { + /// Amount in satoshi + pub amount: Amount, + /// encrypted secret message (B_) + #[serde(rename = "B_")] + pub b: PublicKey, +} + +/// Blinded Messages [NUT-00] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlindedMessages { + /// Blinded messages + pub blinded_messages: Vec, + /// Secrets + pub secrets: Vec, + /// Rs + pub rs: Vec, + /// Amounts + pub amounts: Vec, +} + +impl BlindedMessages { + /// Outputs for speceifed amount with random secret + pub fn random(amount: Amount) -> Result { + let mut blinded_messages = BlindedMessages::default(); + + for amount in split_amount(amount) { + let secret = generate_secret(); + let (blinded, r) = blind_message(secret.as_bytes(), None)?; + + let blinded_message = BlindedMessage { amount, b: blinded }; + + blinded_messages.secrets.push(secret); + blinded_messages.blinded_messages.push(blinded_message); + blinded_messages.rs.push(r.into()); + blinded_messages.amounts.push(amount); + } + + Ok(blinded_messages) + } + + /// Blank Outputs used for NUT-08 change + pub fn blank(fee_reserve: Amount) -> Result { + let mut blinded_messages = BlindedMessages::default(); + + let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat()); + + let count = (fee_reserve + .to_float_in(bitcoin::Denomination::Satoshi) + .log2() + .ceil() as u64) + .max(1); + + for _i in 0..count { + let secret = generate_secret(); + let (blinded, r) = blind_message(secret.as_bytes(), None)?; + + let blinded_message = BlindedMessage { + amount: Amount::ZERO, + b: blinded, + }; + + blinded_messages.secrets.push(secret); + blinded_messages.blinded_messages.push(blinded_message); + blinded_messages.rs.push(r.into()); + blinded_messages.amounts.push(Amount::ZERO); + } + + Ok(blinded_messages) + } +} + +/// Promise (BlindedSignature) [NUT-00] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlindedSignature { + pub id: String, + pub amount: Amount, + /// blinded signature (C_) on the secret message `B_` of [BlindedMessage] + #[serde(rename = "C_")] + pub c: PublicKey, +} + +/// Proofs [NUT-00] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Proof { + /// Amount in satoshi + pub amount: Amount, + /// Secret message + // #[serde(with = "crate::serde_utils::bytes_base64")] + pub secret: String, + /// Unblinded signature + #[serde(rename = "C")] + pub c: PublicKey, + /// `Keyset id` + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// P2SHScript that specifies the spending condition for this Proof + pub script: Option, +} + +/// List of proofs +pub type Proofs = Vec; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintProofs { + #[serde(with = "serde_url")] + pub mint: Url, + pub proofs: Proofs, +} + +impl MintProofs { + fn new(mint_url: Url, proofs: Proofs) -> Self { + Self { + mint: mint_url, + proofs, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Token { + pub token: Vec, + pub memo: Option, +} + +impl Token { + pub fn new(mint_url: Url, proofs: Proofs, memo: Option) -> Self { + Self { + token: vec![MintProofs::new(mint_url, proofs)], + memo, + } + } + + pub fn token_info(&self) -> (u64, String) { + let mut amount = Amount::ZERO; + + for proofs in &self.token { + for proof in &proofs.proofs { + amount += proof.amount; + } + } + + (amount.to_sat(), self.token[0].mint.to_string()) + } +} + +impl FromStr for Token { + type Err = Error; + + fn from_str(s: &str) -> Result { + if !s.starts_with("cashuA") { + return Err(Error::UnsupportedToken); + } + + let s = s.replace("cashuA", ""); + let decoded = general_purpose::STANDARD.decode(s)?; + let decoded_str = String::from_utf8(decoded)?; + println!("decode: {:?}", decoded_str); + let token: Token = serde_json::from_str(&decoded_str)?; + Ok(token) + } +} + +impl Token { + pub fn convert_to_string(&self) -> Result { + let json_string = serde_json::to_string(self)?; + let encoded = general_purpose::STANDARD.encode(json_string); + Ok(format!("cashuA{}", encoded)) + } +} + +pub mod mint { + use serde::{Deserialize, Serialize}; + + use crate::amount::Amount; + + use super::PublicKey; + + /// Proofs [NUT-00] + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Proof { + /// Amount in satoshi + pub amount: Option, + /// Secret message + // #[serde(with = "crate::serde_utils::bytes_base64")] + pub secret: String, + /// Unblinded signature + #[serde(rename = "C")] + pub c: Option, + /// `Keyset id` + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// P2SHScript that specifies the spending condition for this Proof + pub script: Option, + } + + /// List of proofs + pub type Proofs = Vec; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proof_seralize() { + let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]"; + let proof: Proofs = serde_json::from_str(proof).unwrap(); + + assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva"); + } + + #[test] + fn test_token_str_round_trip() { + let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0="; + let token = Token::from_str(token_str).unwrap(); + + assert_eq!( + token.token[0].mint, + Url::from_str("https://8333.space:3338").unwrap() + ); + assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva"); + + let encoded = &token.convert_to_string().unwrap(); + + let token_data = Token::from_str(encoded).unwrap(); + + assert_eq!(token_data, token); + } + + #[test] + fn test_blank_blinded_messages() { + let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap(); + assert_eq!(b.blinded_messages.len(), 10); + + let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap(); + assert_eq!(b.blinded_messages.len(), 1); + } +} diff --git a/src/nuts/nut01.rs b/src/nuts/nut01.rs new file mode 100644 index 00000000..cb9f908a --- /dev/null +++ b/src/nuts/nut01.rs @@ -0,0 +1,111 @@ +//! Mint public key exchange +// https://github.com/cashubtc/nuts/blob/main/01.md + +use std::collections::BTreeMap; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PublicKey(#[serde(with = "crate::serde_utils::serde_public_key")] k256::PublicKey); + +impl From for k256::PublicKey { + fn from(value: PublicKey) -> k256::PublicKey { + value.0 + } +} + +impl From<&PublicKey> for k256::PublicKey { + fn from(value: &PublicKey) -> k256::PublicKey { + value.0 + } +} + +impl From for PublicKey { + fn from(value: k256::PublicKey) -> Self { + Self(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey); + +impl From for k256::SecretKey { + fn from(value: SecretKey) -> k256::SecretKey { + value.0 + } +} + +impl From for SecretKey { + fn from(value: k256::SecretKey) -> Self { + Self(value) + } +} + +/// Mint Keys [NUT-01] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Keys(BTreeMap); + +impl Keys { + pub fn new(keys: BTreeMap) -> Self { + Self(keys) + } + + pub fn keys(&self) -> BTreeMap { + self.0.clone() + } + + pub fn amount_key(&self, amount: &u64) -> Option { + self.0.get(amount).cloned() + } + + pub fn as_hashmap(&self) -> HashMap { + self.0 + .iter() + .map(|(k, v)| (k.to_owned(), hex::encode(v.0.to_sec1_bytes()))) + .collect() + } +} + +impl From for Keys { + fn from(keys: mint::Keys) -> Self { + Self( + keys.0 + .iter() + .map(|(amount, keypair)| (*amount, keypair.public_key.clone())) + .collect(), + ) + } +} + +pub mod mint { + use std::collections::BTreeMap; + + use k256::SecretKey; + use serde::Deserialize; + use serde::Serialize; + + use super::PublicKey; + use crate::serde_utils; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Keys(pub BTreeMap); + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct KeyPair { + pub public_key: PublicKey, + #[serde(with = "serde_utils::serde_secret_key")] + pub secret_key: SecretKey, + } + + impl KeyPair { + pub fn from_secret_key(secret_key: SecretKey) -> Self { + Self { + public_key: secret_key.public_key().into(), + secret_key, + } + } + } +} diff --git a/src/keyset.rs b/src/nuts/nut02.rs similarity index 80% rename from src/keyset.rs rename to src/nuts/nut02.rs index 5e3df597..63a11725 100644 --- a/src/keyset.rs +++ b/src/nuts/nut02.rs @@ -1,7 +1,6 @@ -//! Keysets +//! Keysets and keyset ID +// https://github.com/cashubtc/nuts/blob/main/02.md -use std::collections::BTreeMap; -use std::collections::HashMap; use std::collections::HashSet; use base64::{engine::general_purpose, Engine as _}; @@ -9,77 +8,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct PublicKey(#[serde(with = "crate::serde_utils::serde_public_key")] k256::PublicKey); - -impl From for k256::PublicKey { - fn from(value: PublicKey) -> k256::PublicKey { - value.0 - } -} - -impl From for PublicKey { - fn from(value: k256::PublicKey) -> Self { - Self(value) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct SecretKey(#[serde(with = "crate::serde_utils::serde_secret_key")] k256::SecretKey); - -impl From for k256::SecretKey { - fn from(value: SecretKey) -> k256::SecretKey { - value.0 - } -} - -impl From for SecretKey { - fn from(value: k256::SecretKey) -> Self { - Self(value) - } -} - -/// Mint Keys [NUT-01] -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct Keys(BTreeMap); - -impl Keys { - pub fn new(keys: BTreeMap) -> Self { - Self(keys) - } - - pub fn amount_key(&self, amount: &u64) -> Option { - self.0.get(amount).cloned() - } - - pub fn as_hashmap(&self) -> HashMap { - self.0 - .iter() - .map(|(k, v)| (k.to_owned(), hex::encode(v.0.to_sec1_bytes()))) - .collect() - } - - pub fn id(&self) -> String { - /* 1 - sort keyset by amount - * 2 - concatenate all (sorted) public keys to one string - * 3 - HASH_SHA256 the concatenated public keys - * 4 - take the first 12 characters of the hash - */ - - let pubkeys_concat = self - .0 - .values() - .map(|pubkey| hex::encode(&pubkey.0.to_sec1_bytes())) - .collect::>() - .join(""); - - let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes())); - - hash[0..12].to_string() - } -} +use super::nut01::Keys; /// Mint Keysets [NUT-02] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -94,17 +23,6 @@ pub struct KeySet { pub keys: Keys, } -impl From for Keys { - fn from(keys: mint::Keys) -> Self { - Self( - keys.0 - .iter() - .map(|(amount, keypair)| (*amount, keypair.public_key.clone())) - .collect(), - ) - } -} - impl From for KeySet { fn from(keyset: mint::KeySet) -> Self { Self { @@ -114,6 +32,27 @@ impl From for KeySet { } } +impl Keys { + pub fn id(&self) -> String { + /* 1 - sort keyset by amount + * 2 - concatenate all (sorted) public keys to one string + * 3 - HASH_SHA256 the concatenated public keys + * 4 - take the first 12 characters of the hash + */ + + let pubkeys_concat = self + .keys() + .values() + .map(|pubkey| hex::encode(k256::PublicKey::from(pubkey).to_sec1_bytes())) + .collect::>() + .join(""); + + let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes())); + + hash[0..12].to_string() + } +} + pub mod mint { use std::collections::BTreeMap; @@ -125,11 +64,7 @@ pub mod mint { use serde::Deserialize; use serde::Serialize; - use super::PublicKey; - use crate::serde_utils; - - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - pub struct Keys(pub BTreeMap); + use crate::nuts::nut01::mint::{KeyPair, Keys}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct KeySet { @@ -184,7 +119,9 @@ pub mod mint { let pubkeys_concat = map .values() - .map(|keypair| hex::encode(&keypair.public_key.0.to_sec1_bytes())) + .map(|keypair| { + hex::encode(k256::PublicKey::from(&keypair.public_key).to_sec1_bytes()) + }) .collect::>() .join(""); @@ -193,22 +130,6 @@ pub mod mint { hash[0..12].to_string() } } - - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - pub struct KeyPair { - pub public_key: PublicKey, - #[serde(with = "serde_utils::serde_secret_key")] - pub secret_key: SecretKey, - } - - impl KeyPair { - fn from_secret_key(secret_key: SecretKey) -> Self { - Self { - public_key: secret_key.public_key().into(), - secret_key, - } - } - } } #[cfg(test)] diff --git a/src/nuts/nut03.rs b/src/nuts/nut03.rs new file mode 100644 index 00000000..3419ce19 --- /dev/null +++ b/src/nuts/nut03.rs @@ -0,0 +1,15 @@ +//! Request mint +// https://github.com/cashubtc/nuts/blob/main/03.md + +use serde::{Deserialize, Serialize}; + +pub use crate::Invoice; + +/// Mint request response [NUT-03] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RequestMintResponse { + /// Bolt11 payment request + pub pr: Invoice, + /// Random hash MUST not be the hash of invoice + pub hash: String, +} diff --git a/src/nuts/nut04.rs b/src/nuts/nut04.rs new file mode 100644 index 00000000..9e2ff784 --- /dev/null +++ b/src/nuts/nut04.rs @@ -0,0 +1,27 @@ +//! Mint Tokens +// https://github.com/cashubtc/nuts/blob/main/04.md +use serde::{Deserialize, Serialize}; + +use super::nut00::{BlindedMessage, BlindedSignature}; +use crate::Amount; + +/// Post Mint Request [NUT-04] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintRequest { + pub outputs: Vec, +} + +impl MintRequest { + pub fn total_amount(&self) -> Amount { + self.outputs + .iter() + .map(|BlindedMessage { amount, .. }| *amount) + .sum() + } +} + +/// Post Mint Response [NUT-04] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PostMintResponse { + pub promises: Vec, +} diff --git a/src/nuts/nut05.rs b/src/nuts/nut05.rs new file mode 100644 index 00000000..be2c719c --- /dev/null +++ b/src/nuts/nut05.rs @@ -0,0 +1,51 @@ +//! Melting Tokens +// https://github.com/cashubtc/nuts/blob/main/05.md + +use serde::{Deserialize, Serialize}; + +use super::nut00::Proofs; +use crate::amount::Amount; +use crate::error::Error; +use crate::Invoice; + +/// Check Fees Response [NUT-05] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CheckFeesResponse { + /// Expected Mac Fee in satoshis + pub fee: Amount, +} + +/// Check Fees request [NUT-05] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CheckFeesRequest { + /// Lighting Invoice + pub pr: Invoice, +} + +/// Melt Request [NUT-05] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltRequest { + pub proofs: Proofs, + /// bollt11 + pub pr: Invoice, +} + +impl MeltRequest { + pub fn proofs_amount(&self) -> Amount { + self.proofs.iter().map(|proof| proof.amount).sum() + } + + pub fn invoice_amount(&self) -> Result { + match self.pr.amount_milli_satoshis() { + Some(value) => Ok(Amount::from_sat(value)), + None => Err(Error::InvoiceAmountUndefined), + } + } +} + +/// Melt Response [NUT-05] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltResponse { + pub paid: bool, + pub preimage: Option, +} diff --git a/src/nuts/nut06.rs b/src/nuts/nut06.rs new file mode 100644 index 00000000..c4a113fb --- /dev/null +++ b/src/nuts/nut06.rs @@ -0,0 +1,57 @@ +//! Split +// https://github.com/cashubtc/nuts/blob/main/06.md +use serde::{Deserialize, Serialize}; + +use crate::amount::Amount; +use crate::nuts::nut00::{BlindedMessage, BlindedMessages, Proofs}; + +use super::nut00::BlindedSignature; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SplitPayload { + pub keep_blinded_messages: BlindedMessages, + pub send_blinded_messages: BlindedMessages, + pub split_payload: SplitRequest, +} + +/// Split Request [NUT-06] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SplitRequest { + pub amount: Amount, + pub proofs: Proofs, + pub outputs: Vec, +} + +impl SplitRequest { + pub fn proofs_amount(&self) -> Amount { + self.proofs.iter().map(|proof| proof.amount).sum() + } + pub fn output_amount(&self) -> Amount { + self.outputs.iter().map(|proof| proof.amount).sum() + } +} + +/// Split Response [NUT-06] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SplitResponse { + /// Promises to keep + pub fst: Vec, + /// Promises to send + pub snd: Vec, +} + +impl SplitResponse { + pub fn change_amount(&self) -> Amount { + self.fst + .iter() + .map(|BlindedSignature { amount, .. }| *amount) + .sum() + } + + pub fn target_amount(&self) -> Amount { + self.snd + .iter() + .map(|BlindedSignature { amount, .. }| *amount) + .sum() + } +} diff --git a/src/nuts/nut07.rs b/src/nuts/nut07.rs new file mode 100644 index 00000000..ffbd6466 --- /dev/null +++ b/src/nuts/nut07.rs @@ -0,0 +1,20 @@ +//! Spendable Check +// https://github.com/cashubtc/nuts/blob/main/07.md + +use serde::{Deserialize, Serialize}; + +use super::nut00::mint; + +/// Check spendabale request [NUT-07] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CheckSpendableRequest { + pub proofs: mint::Proofs, +} + +/// Check Spendable Response [NUT-07] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CheckSpendableResponse { + /// booleans indicating whether the provided Proof is still spendable. + /// In same order as provided proofs + pub spendable: Vec, +} diff --git a/src/nuts/nut08.rs b/src/nuts/nut08.rs new file mode 100644 index 00000000..0b3092b8 --- /dev/null +++ b/src/nuts/nut08.rs @@ -0,0 +1,42 @@ +//! Lightning fee return +// https://github.com/cashubtc/nuts/blob/main/08.md + +use lightning_invoice::Invoice; +use serde::{Deserialize, Serialize}; + +use crate::{error::Error, Amount}; + +use super::nut00::{BlindedMessage, BlindedSignature, Proofs}; + +/// Melt Request [NUT-08] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltRequest { + pub proofs: Proofs, + /// bollt11 + pub pr: Invoice, + /// Blinded Message that can be used to return change [NUT-08] + /// Amount field of blindedMessages `SHOULD` be set to zero + pub outputs: Option>, +} + +impl MeltRequest { + pub fn proofs_amount(&self) -> Amount { + self.proofs.iter().map(|proof| proof.amount).sum() + } + + pub fn invoice_amount(&self) -> Result { + match self.pr.amount_milli_satoshis() { + Some(value) => Ok(Amount::from_sat(value)), + None => Err(Error::InvoiceAmountUndefined), + } + } +} + +/// Melt Response [NUT-08] +/// Lightning fee return [NUT-08] if change is defined +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MeltResponse { + pub paid: bool, + pub preimage: Option, + pub change: Option>, +} diff --git a/src/nuts/nut09.rs b/src/nuts/nut09.rs new file mode 100644 index 00000000..a7b3cebd --- /dev/null +++ b/src/nuts/nut09.rs @@ -0,0 +1,61 @@ +//! Mint Information +// https://github.com/cashubtc/nuts/blob/main/09.md + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use super::nut01::PublicKey; + +/// Mint Version +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MintVersion { + pub name: String, + pub version: String, +} + +impl Serialize for MintVersion { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let combined = format!("{}/{}", self.name, self.version); + serializer.serialize_str(&combined) + } +} + +impl<'de> Deserialize<'de> for MintVersion { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let combined = String::deserialize(deserializer)?; + let parts: Vec<&str> = combined.split('/').collect(); + if parts.len() != 2 { + return Err(serde::de::Error::custom("Invalid input string")); + } + Ok(MintVersion { + name: parts[0].to_string(), + version: parts[1].to_string(), + }) + } +} + +/// Mint Info [NIP-09] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintInfo { + /// name of the mint and should be recognizable + pub name: Option, + /// hex pubkey of the mint + pub pubkey: Option, + /// implementation name and the version running + pub version: Option, + /// short description of the mint + pub description: Option, + /// long description + pub description_long: Option, + /// contact methods to reach the mint operator + pub contact: Vec>, + /// shows which NUTs the mint supports + pub nuts: Vec, + /// message of the day that the wallet must display to the user + pub motd: Option, +} diff --git a/src/types.rs b/src/types.rs index 1225407a..1f6689d8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,262 +1,8 @@ //! Types for `cashu-crab` -use std::str::FromStr; +use serde::{Deserialize, Serialize}; -use base64::{engine::general_purpose, Engine as _}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Url; - -use crate::keyset::{self, PublicKey}; -use crate::utils::generate_secret; -use crate::Amount; -pub use crate::Invoice; -use crate::{dhke::blind_message, error::Error, mint, serde_utils::serde_url, utils::split_amount}; - -/// Blinded Message [NUT-00] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BlindedMessage { - /// Amount in satoshi - pub amount: Amount, - /// encrypted secret message (B_) - #[serde(rename = "B_")] - pub b: PublicKey, -} - -/// Blinded Messages [NUT-00] -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BlindedMessages { - /// Blinded messages - pub blinded_messages: Vec, - /// Secrets - pub secrets: Vec, - /// Rs - pub rs: Vec, - /// Amounts - pub amounts: Vec, -} - -impl BlindedMessages { - /// Outputs for speceifed amount with random secret - pub fn random(amount: Amount) -> Result { - let mut blinded_messages = BlindedMessages::default(); - - for amount in split_amount(amount) { - let secret = generate_secret(); - let (blinded, r) = blind_message(secret.as_bytes(), None)?; - - let blinded_message = BlindedMessage { amount, b: blinded }; - - blinded_messages.secrets.push(secret); - blinded_messages.blinded_messages.push(blinded_message); - blinded_messages.rs.push(r.into()); - blinded_messages.amounts.push(amount); - } - - Ok(blinded_messages) - } - - /// Blank Outputs used for NUT-08 change - pub fn blank(fee_reserve: Amount) -> Result { - let mut blinded_messages = BlindedMessages::default(); - - let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat()); - - let count = (fee_reserve - .to_float_in(bitcoin::Denomination::Satoshi) - .log2() - .ceil() as u64) - .max(1); - - for _i in 0..count { - let secret = generate_secret(); - let (blinded, r) = blind_message(secret.as_bytes(), None)?; - - let blinded_message = BlindedMessage { - amount: Amount::ZERO, - b: blinded, - }; - - blinded_messages.secrets.push(secret); - blinded_messages.blinded_messages.push(blinded_message); - blinded_messages.rs.push(r.into()); - blinded_messages.amounts.push(Amount::ZERO); - } - - Ok(blinded_messages) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SplitPayload { - pub keep_blinded_messages: BlindedMessages, - pub send_blinded_messages: BlindedMessages, - pub split_payload: SplitRequest, -} - -/// Promise (BlindedSignature) [NUT-00] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Promise { - pub id: String, - pub amount: Amount, - /// blinded signature (C_) on the secret message `B_` of [BlindedMessage] - #[serde(rename = "C_")] - pub c: PublicKey, -} - -/// Proofs [NUT-00] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Proof { - /// Amount in satoshi - pub amount: Amount, - /// Secret message - // #[serde(with = "crate::serde_utils::bytes_base64")] - pub secret: String, - /// Unblinded signature - #[serde(rename = "C")] - pub c: PublicKey, - /// `Keyset id` - pub id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - /// P2SHScript that specifies the spending condition for this Proof - pub script: Option, -} - -/// List of proofs -pub type Proofs = Vec; - -/// Mint request response [NUT-03] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RequestMintResponse { - /// Bolt11 payment request - pub pr: Invoice, - /// Random hash MUST not be the hash of invoice - pub hash: String, -} - -/// Post Mint Request [NUT-04] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MintRequest { - pub outputs: Vec, -} - -impl MintRequest { - pub fn total_amount(&self) -> Amount { - self.outputs - .iter() - .map(|BlindedMessage { amount, .. }| *amount) - .sum() - } -} - -/// Post Mint Response [NUT-05] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PostMintResponse { - pub promises: Vec, -} - -/// Check Fees Response [NUT-05] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CheckFeesResponse { - /// Expected Mac Fee in satoshis - pub fee: Amount, -} - -/// Check Fees request [NUT-05] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CheckFeesRequest { - /// Lighting Invoice - pub pr: Invoice, -} - -/// Melt Request [NUT-05] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MeltRequest { - pub proofs: Proofs, - /// bollt11 - pub pr: Invoice, - /// Blinded Message that can be used to return change [NUT-08] - /// Amount field of blindedMessages `SHOULD` be set to zero - pub outputs: Option>, -} - -impl MeltRequest { - pub fn proofs_amount(&self) -> Amount { - self.proofs.iter().map(|proof| proof.amount).sum() - } - - pub fn invoice_amount(&self) -> Result { - match self.pr.amount_milli_satoshis() { - Some(value) => Ok(Amount::from_sat(value)), - None => Err(Error::InvoiceAmountUndefined), - } - } -} - -/// Melt Response [NUT-05] -/// Lightning fee return [NUT-08] if change is defined -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MeltResponse { - pub paid: bool, - pub preimage: Option, - pub change: Option>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Melted { - pub paid: bool, - pub preimage: Option, - pub change: Option, -} - -/// Split Request [NUT-06] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SplitRequest { - pub amount: Amount, - pub proofs: Proofs, - pub outputs: Vec, -} - -impl SplitRequest { - pub fn proofs_amount(&self) -> Amount { - self.proofs.iter().map(|proof| proof.amount).sum() - } - pub fn output_amount(&self) -> Amount { - self.outputs.iter().map(|proof| proof.amount).sum() - } -} - -/// Split Response [NUT-06] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SplitResponse { - /// Promises to keep - pub fst: Vec, - /// Promises to send - pub snd: Vec, -} - -impl SplitResponse { - pub fn change_amount(&self) -> Amount { - self.fst.iter().map(|Promise { amount, .. }| *amount).sum() - } - - pub fn target_amount(&self) -> Amount { - self.snd.iter().map(|Promise { amount, .. }| *amount).sum() - } -} - -/// Check spendabale request [NUT-07] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CheckSpendableRequest { - pub proofs: mint::Proofs, -} - -/// Check Spendable Response [NUT-07] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CheckSpendableResponse { - /// booleans indicating whether the provided Proof is still spendable. - /// In same order as provided proofs - pub spendable: Vec, -} +use crate::nuts::nut00::{mint, Proofs}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProofsStatus { @@ -269,166 +15,3 @@ pub struct SendProofs { pub change_proofs: Proofs, pub send_proofs: Proofs, } - -/// Mint Version -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MintVersion { - pub name: String, - pub version: String, -} - -impl Serialize for MintVersion { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let combined = format!("{}/{}", self.name, self.version); - serializer.serialize_str(&combined) - } -} - -impl<'de> Deserialize<'de> for MintVersion { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let combined = String::deserialize(deserializer)?; - let parts: Vec<&str> = combined.split('/').collect(); - if parts.len() != 2 { - return Err(serde::de::Error::custom("Invalid input string")); - } - Ok(MintVersion { - name: parts[0].to_string(), - version: parts[1].to_string(), - }) - } -} - -/// Mint Info [NIP-09] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MintInfo { - /// name of the mint and should be recognizable - pub name: Option, - /// hex pubkey of the mint - pub pubkey: Option, - /// implementation name and the version running - pub version: Option, - /// short description of the mint - pub description: Option, - /// long description - pub description_long: Option, - /// contact methods to reach the mint operator - pub contact: Vec>, - /// shows which NUTs the mint supports - pub nuts: Vec, - /// message of the day that the wallet must display to the user - pub motd: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MintProofs { - #[serde(with = "serde_url")] - pub mint: Url, - pub proofs: Proofs, -} - -impl MintProofs { - fn new(mint_url: Url, proofs: Proofs) -> Self { - Self { - mint: mint_url, - proofs, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Token { - pub token: Vec, - pub memo: Option, -} - -impl Token { - pub fn new(mint_url: Url, proofs: Proofs, memo: Option) -> Self { - Self { - token: vec![MintProofs::new(mint_url, proofs)], - memo, - } - } - - pub fn token_info(&self) -> (u64, String) { - let mut amount = Amount::ZERO; - - for proofs in &self.token { - for proof in &proofs.proofs { - amount += proof.amount; - } - } - - (amount.to_sat(), self.token[0].mint.to_string()) - } -} - -impl FromStr for Token { - type Err = Error; - - fn from_str(s: &str) -> Result { - if !s.starts_with("cashuA") { - return Err(Error::UnsupportedToken); - } - - let s = s.replace("cashuA", ""); - let decoded = general_purpose::STANDARD.decode(s)?; - let decoded_str = String::from_utf8(decoded)?; - println!("decode: {:?}", decoded_str); - let token: Token = serde_json::from_str(&decoded_str)?; - Ok(token) - } -} - -impl Token { - pub fn convert_to_string(&self) -> Result { - let json_string = serde_json::to_string(self)?; - let encoded = general_purpose::STANDARD.encode(json_string); - Ok(format!("cashuA{}", encoded)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_proof_seralize() { - let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]"; - let proof: Proofs = serde_json::from_str(proof).unwrap(); - - assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva"); - } - - #[test] - fn test_token_str_round_trip() { - let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0="; - let token = Token::from_str(token_str).unwrap(); - - assert_eq!( - token.token[0].mint, - Url::from_str("https://8333.space:3338").unwrap() - ); - assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva"); - - let encoded = &token.convert_to_string().unwrap(); - - let token_data = Token::from_str(encoded).unwrap(); - - assert_eq!(token_data, token); - } - - #[test] - fn test_blank_blinded_messages() { - let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap(); - assert_eq!(b.blinded_messages.len(), 10); - - let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap(); - assert_eq!(b.blinded_messages.len(), 1); - } -}