Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 241 additions & 8 deletions crates/cdk-common/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,124 @@ impl Default for QuoteTTL {
}
}

/// Mint Fee Reserve
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FeeReserve {
/// Absolute expected min fee
pub min_fee_reserve: Amount,
/// Percentage expected fee
pub percent_fee_reserve: f32,
}

/// CDK Version
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct IssuerVersion {
/// Implementation name (e.g., "cdk", "nutshell")
pub implementation: String,
/// Major version
pub major: u16,
/// Minor version
pub minor: u16,
/// Patch version
pub patch: u16,
}

impl IssuerVersion {
/// Create new [`IssuerVersion`]
pub fn new(implementation: String, major: u16, minor: u16, patch: u16) -> Self {
Self {
implementation,
major,
minor,
patch,
}
}
}

impl std::fmt::Display for IssuerVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}.{}.{}",
self.implementation, self.major, self.minor, self.patch
)
}
}

impl PartialOrd for IssuerVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.implementation != other.implementation {
return None;
}

match self.major.cmp(&other.major) {
std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
std::cmp::Ordering::Equal => Some(self.patch.cmp(&other.patch)),
other => Some(other),
},
other => Some(other),
}
}
}

impl std::str::FromStr for IssuerVersion {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (implementation, version_str) = match s.split_once('/') {
Some((impl_str, ver_str)) => (impl_str.to_string(), ver_str),
None => ("cdk".to_string(), s),
};

let parts: Vec<&str> = version_str.splitn(3, '.').collect();
if parts.len() != 3 {
return Err(Error::Custom(format!("Invalid version string: {}", s)));
}

let major = parts[0]
.parse()
.map_err(|_| Error::Custom(format!("Invalid major version: {}", parts[0])))?;
let minor = parts[1]
.parse()
.map_err(|_| Error::Custom(format!("Invalid minor version: {}", parts[1])))?;

// Handle patch version with optional suffixes like -rc1
let patch_str = parts[2];
let patch_end = patch_str
.find(|c: char| !c.is_numeric())
.unwrap_or(patch_str.len());
let patch = patch_str[..patch_end]
.parse()
.map_err(|_| Error::Custom(format!("Invalid patch version: {}", parts[2])))?;

Ok(Self {
implementation,
major,
minor,
patch,
})
}
}

impl Serialize for IssuerVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for IssuerVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
std::str::FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down Expand Up @@ -358,13 +476,128 @@ mod tests {
};
assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
}
}

/// Mint Fee Reserve
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FeeReserve {
/// Absolute expected min fee
pub min_fee_reserve: Amount,
/// Percentage expected fee
pub percent_fee_reserve: f32,
use super::IssuerVersion;

#[test]
fn test_version_parsing() {
// Test legacy format (defaults to cdk)
let v = IssuerVersion::from_str("0.1.0").unwrap();
assert_eq!(v.implementation, "cdk");
assert_eq!(v.major, 0);
assert_eq!(v.minor, 1);
assert_eq!(v.patch, 0);
assert_eq!(v.to_string(), "cdk/0.1.0");

// Test explicit cdk format
let v = IssuerVersion::from_str("cdk/1.2.3").unwrap();
assert_eq!(v.implementation, "cdk");
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
assert_eq!(v.to_string(), "cdk/1.2.3");

// Test nutshell format
let v = IssuerVersion::from_str("nutshell/0.16.0").unwrap();
assert_eq!(v.implementation, "nutshell");
assert_eq!(v.major, 0);
assert_eq!(v.minor, 16);
assert_eq!(v.patch, 0);
assert_eq!(v.to_string(), "nutshell/0.16.0");
}

#[test]
fn test_version_ordering() {
let v1 = IssuerVersion::from_str("cdk/0.1.0").unwrap();
let v2 = IssuerVersion::from_str("cdk/0.1.1").unwrap();
let v3 = IssuerVersion::from_str("cdk/0.2.0").unwrap();
let v4 = IssuerVersion::from_str("cdk/1.0.0").unwrap();

assert!(v1 < v2);
assert!(v2 < v3);
assert!(v3 < v4);
assert!(v1 < v4);

// Test mixed implementations
let v_nutshell = IssuerVersion::from_str("nutshell/0.1.0").unwrap();
assert_eq!(v1.partial_cmp(&v_nutshell), None);
assert!(!(v1 < v_nutshell));
assert!(!(v1 > v_nutshell));
assert!(!(v1 == v_nutshell));
}

#[test]
fn test_version_serialization() {
let v = IssuerVersion::from_str("cdk/0.14.2").unwrap();
let json = serde_json::to_string(&v).unwrap();
assert_eq!(json, "\"cdk/0.14.2\"");

let v_deserialized: IssuerVersion = serde_json::from_str(&json).unwrap();
assert_eq!(v, v_deserialized);

// Legacy deserialization (string without prefix)
let json_legacy = "\"0.14.2\"";
let v_legacy: IssuerVersion = serde_json::from_str(json_legacy).unwrap();
assert_eq!(v_legacy.implementation, "cdk");
assert_eq!(v_legacy.major, 0);
assert_eq!(v_legacy.minor, 14);
assert_eq!(v_legacy.patch, 2);
}

#[test]
fn test_cdk_version_parsing_with_suffix() {
let version_str = "0.15.0-rc1";
let version = IssuerVersion::from_str(version_str).unwrap();
assert_eq!(version.implementation, "cdk");
assert_eq!(version.major, 0);
assert_eq!(version.minor, 15);
assert_eq!(version.patch, 0);
}

#[test]
fn test_cdk_version_parsing_standard() {
let version_str = "0.15.0";
let version = IssuerVersion::from_str(version_str).unwrap();
assert_eq!(version.implementation, "cdk");
assert_eq!(version.major, 0);
assert_eq!(version.minor, 15);
assert_eq!(version.patch, 0);
}

#[test]
fn test_cdk_version_parsing_complex_suffix() {
let version_str = "0.15.0-beta.1+build123";
let version = IssuerVersion::from_str(version_str).unwrap();
assert_eq!(version.implementation, "cdk");
assert_eq!(version.major, 0);
assert_eq!(version.minor, 15);
assert_eq!(version.patch, 0);
}

#[test]
fn test_cdk_version_parsing_invalid() {
let version_str = "0.15";
assert!(IssuerVersion::from_str(version_str).is_err());

let version_str = "0.15.a";
assert!(IssuerVersion::from_str(version_str).is_err());
}

#[test]
fn test_cdk_version_parsing_with_implementation() {
let version_str = "nutshell/0.16.2";
let version = IssuerVersion::from_str(version_str).unwrap();
assert_eq!(version.implementation, "nutshell");
assert_eq!(version.major, 0);
assert_eq!(version.minor, 16);
assert_eq!(version.patch, 2);
}

#[test]
fn test_cdk_version_comparison_different_implementations() {
let v1 = IssuerVersion::from_str("cdk/0.15.0").unwrap();
let v2 = IssuerVersion::from_str("nutshell/0.15.0").unwrap();

assert_eq!(v1.partial_cmp(&v2), None);
}
}
11 changes: 11 additions & 0 deletions crates/cdk-common/src/database/mint/test/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::str::FromStr;
use bitcoin::bip32::DerivationPath;
use cashu::{CurrencyUnit, Id};

use crate::common::IssuerVersion;
use crate::database::mint::{Database, Error, KeysDatabase};
use crate::mint::MintKeySetInfo;

Expand All @@ -29,6 +30,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add keyset info
Expand All @@ -44,6 +46,7 @@ where
assert_eq!(retrieved.unit, keyset_info.unit);
assert_eq!(retrieved.active, keyset_info.active);
assert_eq!(retrieved.amounts, keyset_info.amounts);
assert_eq!(retrieved.issuer_version, keyset_info.issuer_version);
}

/// Test adding duplicate keyset info is idempotent
Expand All @@ -62,6 +65,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add keyset info first time
Expand Down Expand Up @@ -97,6 +101,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

let keyset_id2 = Id::from_str("00916bbf7ef91a37").unwrap();
Expand All @@ -110,6 +115,7 @@ where
derivation_path_index: Some(1),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add keyset infos
Expand Down Expand Up @@ -141,6 +147,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add keyset info
Expand Down Expand Up @@ -173,6 +180,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

let keyset_id_usd = Id::from_str("00916bbf7ef91a37").unwrap();
Expand All @@ -186,6 +194,7 @@ where
derivation_path_index: Some(1),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add keyset infos and set as active
Expand Down Expand Up @@ -223,6 +232,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

let keyset_id2 = Id::from_str("00916bbf7ef91a37").unwrap();
Expand All @@ -236,6 +246,7 @@ where
derivation_path_index: Some(1),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};

// Add both keysets and set first as active
Expand Down
2 changes: 2 additions & 0 deletions crates/cdk-common/src/database/mint/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use bitcoin::bip32::DerivationPath;
use cashu::CurrencyUnit;

use super::*;
use crate::common::IssuerVersion;
use crate::database::KVStoreDatabase;
use crate::mint::MintKeySetInfo;

Expand Down Expand Up @@ -49,6 +50,7 @@ where
derivation_path_index: Some(0),
input_fee_ppk: 0,
amounts: standard_keyset_amounts(32),
issuer_version: IssuerVersion::from_str("0.1.0").ok(),
};
let mut writer = db.begin_transaction().await.expect("db.begin()");
writer.add_keyset_info(keyset_info).await.unwrap();
Expand Down
4 changes: 3 additions & 1 deletion crates/cdk-common/src/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use uuid::Uuid;

use crate::nuts::{MeltQuoteState, MintQuoteState};
use crate::payment::PaymentIdentifier;
use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
use crate::{common::IssuerVersion, Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};

/// Operation kind for saga persistence
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -880,6 +880,8 @@ pub struct MintKeySetInfo {
pub input_fee_ppk: u64,
/// Final expiry
pub final_expiry: Option<u64>,
/// Issuer Version
pub issuer_version: Option<IssuerVersion>,
}

/// Default fee
Expand Down
Loading
Loading