refactor: nuts

This commit is contained in:
thesimplekid
2023-06-21 23:12:55 -04:00
parent c4bff1a724
commit fa07c3632f
18 changed files with 745 additions and 631 deletions

View File

@@ -13,7 +13,6 @@ impl Amount {
pub fn split(&self) -> Vec<Self> {
let sats = self.0.to_sat();
(0_u64..64)
.into_iter()
.rev()
.filter_map(|bit| {
let part = 1 << bit;

View File

@@ -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<Melted, Error> {
) -> Result<MeltResponse, Error> {
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<String>) -> Result<String, Error> {

View File

@@ -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<keyset::Response, Error> {
pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
let url = self.mint_url.join("keysets")?;
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<keyset::Response, serde_json::Error> =
let response: Result<nut02::Response, serde_json::Error> =
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<mint::Proof>,
proofs: &Vec<nut00::mint::Proof>,
) -> Result<CheckSpendableResponse, Error> {
let url = self.mint_url.join("check")?;
let request = CheckSpendableRequest {

View File

@@ -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<Promise>,
rs: Vec<keyset::SecretKey>,
promises: Vec<BlindedSignature>,
rs: Vec<nut01::SecretKey>,
secrets: Vec<String>,
keys: &Keys,
) -> Result<Proofs, Error> {

View File

@@ -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;

View File

@@ -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<String, mint::KeySet>,
pub active_keyset: nut02::mint::KeySet,
pub inactive_keysets: HashMap<String, nut02::mint::KeySet>,
pub spent_secrets: HashSet<String>,
}
@@ -29,12 +25,12 @@ impl Mint {
pub fn new(
secret: &str,
derivation_path: &str,
inactive_keysets: HashMap<String, mint::KeySet>,
inactive_keysets: HashMap<String, nut02::mint::KeySet>,
spent_secrets: HashSet<String>,
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<keyset::KeySet> {
if &self.active_keyset.id == id {
pub fn keyset(&self, id: &str) -> Option<nut02::KeySet> {
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<PostMintResponse, Error> {
mint_request: nut04::MintRequest,
) -> Result<nut04::PostMintResponse, Error> {
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<Promise, Error> {
fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result<BlindedSignature, Error> {
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<String, Error> {
fn verify_proof(&self, proof: &Proof) -> Result<String, Error> {
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<Amount>,
/// Secret message
// #[serde(with = "crate::serde_utils::bytes_base64")]
pub secret: String,
/// Unblinded signature
#[serde(rename = "C")]
pub c: Option<PublicKey>,
/// `Keyset id`
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// P2SHScript that specifies the spending condition for this Proof
pub script: Option<String>,
}
/// List of proofs
pub type Proofs = Vec<Proof>;

10
src/nuts/mod.rs Normal file
View File

@@ -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;

256
src/nuts/nut00.rs Normal file
View File

@@ -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<BlindedMessage>,
/// Secrets
pub secrets: Vec<String>,
/// Rs
pub rs: Vec<nut01::SecretKey>,
/// Amounts
pub amounts: Vec<Amount>,
}
impl BlindedMessages {
/// Outputs for speceifed amount with random secret
pub fn random(amount: Amount) -> Result<Self, Error> {
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<Self, Error> {
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// P2SHScript that specifies the spending condition for this Proof
pub script: Option<String>,
}
/// List of proofs
pub type Proofs = Vec<Proof>;
#[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<MintProofs>,
pub memo: Option<String>,
}
impl Token {
pub fn new(mint_url: Url, proofs: Proofs, memo: Option<String>) -> 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<Self, Self::Err> {
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<String, Error> {
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<Amount>,
/// Secret message
// #[serde(with = "crate::serde_utils::bytes_base64")]
pub secret: String,
/// Unblinded signature
#[serde(rename = "C")]
pub c: Option<PublicKey>,
/// `Keyset id`
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// P2SHScript that specifies the spending condition for this Proof
pub script: Option<String>,
}
/// List of proofs
pub type Proofs = Vec<Proof>;
}
#[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);
}
}

111
src/nuts/nut01.rs Normal file
View File

@@ -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<PublicKey> 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<k256::PublicKey> 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<SecretKey> for k256::SecretKey {
fn from(value: SecretKey) -> k256::SecretKey {
value.0
}
}
impl From<k256::SecretKey> 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<u64, PublicKey>);
impl Keys {
pub fn new(keys: BTreeMap<u64, PublicKey>) -> Self {
Self(keys)
}
pub fn keys(&self) -> BTreeMap<u64, PublicKey> {
self.0.clone()
}
pub fn amount_key(&self, amount: &u64) -> Option<PublicKey> {
self.0.get(amount).cloned()
}
pub fn as_hashmap(&self) -> HashMap<u64, String> {
self.0
.iter()
.map(|(k, v)| (k.to_owned(), hex::encode(v.0.to_sec1_bytes())))
.collect()
}
}
impl From<mint::Keys> 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<u64, KeyPair>);
#[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,
}
}
}
}

View File

@@ -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<PublicKey> for k256::PublicKey {
fn from(value: PublicKey) -> k256::PublicKey {
value.0
}
}
impl From<k256::PublicKey> 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<SecretKey> for k256::SecretKey {
fn from(value: SecretKey) -> k256::SecretKey {
value.0
}
}
impl From<k256::SecretKey> 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<u64, PublicKey>);
impl Keys {
pub fn new(keys: BTreeMap<u64, PublicKey>) -> Self {
Self(keys)
}
pub fn amount_key(&self, amount: &u64) -> Option<PublicKey> {
self.0.get(amount).cloned()
}
pub fn as_hashmap(&self) -> HashMap<u64, String> {
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::<Vec<String>>()
.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<mint::Keys> for Keys {
fn from(keys: mint::Keys) -> Self {
Self(
keys.0
.iter()
.map(|(amount, keypair)| (*amount, keypair.public_key.clone()))
.collect(),
)
}
}
impl From<mint::KeySet> for KeySet {
fn from(keyset: mint::KeySet) -> Self {
Self {
@@ -114,6 +32,27 @@ impl From<mint::KeySet> 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::<Vec<String>>()
.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<u64, KeyPair>);
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::<Vec<String>>()
.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)]

15
src/nuts/nut03.rs Normal file
View File

@@ -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,
}

27
src/nuts/nut04.rs Normal file
View File

@@ -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<BlindedMessage>,
}
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<BlindedSignature>,
}

51
src/nuts/nut05.rs Normal file
View File

@@ -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<Amount, Error> {
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<String>,
}

57
src/nuts/nut06.rs Normal file
View File

@@ -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<BlindedMessage>,
}
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<BlindedSignature>,
/// Promises to send
pub snd: Vec<BlindedSignature>,
}
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()
}
}

20
src/nuts/nut07.rs Normal file
View File

@@ -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<bool>,
}

42
src/nuts/nut08.rs Normal file
View File

@@ -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<Vec<BlindedMessage>>,
}
impl MeltRequest {
pub fn proofs_amount(&self) -> Amount {
self.proofs.iter().map(|proof| proof.amount).sum()
}
pub fn invoice_amount(&self) -> Result<Amount, Error> {
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<String>,
pub change: Option<Vec<BlindedSignature>>,
}

61
src/nuts/nut09.rs Normal file
View File

@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let combined = format!("{}/{}", self.name, self.version);
serializer.serialize_str(&combined)
}
}
impl<'de> Deserialize<'de> for MintVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<String>,
/// hex pubkey of the mint
pub pubkey: Option<PublicKey>,
/// implementation name and the version running
pub version: Option<MintVersion>,
/// short description of the mint
pub description: Option<String>,
/// long description
pub description_long: Option<String>,
/// contact methods to reach the mint operator
pub contact: Vec<Vec<String>>,
/// shows which NUTs the mint supports
pub nuts: Vec<String>,
/// message of the day that the wallet must display to the user
pub motd: Option<String>,
}

View File

@@ -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<BlindedMessage>,
/// Secrets
pub secrets: Vec<String>,
/// Rs
pub rs: Vec<keyset::SecretKey>,
/// Amounts
pub amounts: Vec<Amount>,
}
impl BlindedMessages {
/// Outputs for speceifed amount with random secret
pub fn random(amount: Amount) -> Result<Self, Error> {
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<Self, Error> {
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// P2SHScript that specifies the spending condition for this Proof
pub script: Option<String>,
}
/// List of proofs
pub type Proofs = Vec<Proof>;
/// 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<BlindedMessage>,
}
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<Promise>,
}
/// 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<Vec<BlindedMessage>>,
}
impl MeltRequest {
pub fn proofs_amount(&self) -> Amount {
self.proofs.iter().map(|proof| proof.amount).sum()
}
pub fn invoice_amount(&self) -> Result<Amount, Error> {
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<String>,
pub change: Option<Vec<Promise>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Melted {
pub paid: bool,
pub preimage: Option<String>,
pub change: Option<Proofs>,
}
/// Split Request [NUT-06]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SplitRequest {
pub amount: Amount,
pub proofs: Proofs,
pub outputs: Vec<BlindedMessage>,
}
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<Promise>,
/// Promises to send
pub snd: Vec<Promise>,
}
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<bool>,
}
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let combined = format!("{}/{}", self.name, self.version);
serializer.serialize_str(&combined)
}
}
impl<'de> Deserialize<'de> for MintVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<String>,
/// hex pubkey of the mint
pub pubkey: Option<PublicKey>,
/// implementation name and the version running
pub version: Option<MintVersion>,
/// short description of the mint
pub description: Option<String>,
/// long description
pub description_long: Option<String>,
/// contact methods to reach the mint operator
pub contact: Vec<Vec<String>>,
/// shows which NUTs the mint supports
pub nuts: Vec<String>,
/// message of the day that the wallet must display to the user
pub motd: Option<String>,
}
#[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<MintProofs>,
pub memo: Option<String>,
}
impl Token {
pub fn new(mint_url: Url, proofs: Proofs, memo: Option<String>) -> 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<Self, Self::Err> {
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<String, Error> {
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);
}
}