From 169c1a562cb890044aa8d74ccb1b40b733e05003 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sat, 26 Oct 2024 21:39:34 +0100 Subject: [PATCH] feat: support custom unit --- .../cdk-integration-tests/src/init_regtest.rs | 1 + crates/cdk-integration-tests/src/lib.rs | 1 + crates/cdk-integration-tests/tests/mint.rs | 4 +- crates/cdk-mintd/src/main.rs | 1 + crates/cdk/src/mint/keysets.rs | 11 +++- crates/cdk/src/mint/mod.rs | 37 +++++++++---- crates/cdk/src/nuts/nut00/mod.rs | 54 +++++++++++++------ 7 files changed, 81 insertions(+), 28 deletions(-) diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 769a33500..bae3755ac 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -176,6 +176,7 @@ where Arc::new(database), ln_backends, supported_units, + HashMap::new(), ) .await?; diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index eacc3b3a5..8b2caff18 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -82,6 +82,7 @@ pub async fn start_mint( Arc::new(MintMemoryDatabase::default()), ln_backends.clone(), supported_units, + HashMap::new(), ) .await?; let cache_time_to_live = 3600; diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index c86e2dd31..e2ddb6ada 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -48,6 +48,7 @@ async fn new_mint(fee: u64) -> Mint { Arc::new(MintMemoryDatabase::default()), HashMap::new(), supported_units, + HashMap::new(), ) .await .unwrap() @@ -270,7 +271,8 @@ async fn test_swap_unbalanced() -> Result<()> { async fn test_swap_overpay_underpay_fee() -> Result<()> { let mint = new_mint(1).await; - mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1).await?; + mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, HashMap::new()) + .await?; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 4adc2809d..cba39fafe 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -438,6 +438,7 @@ async fn main() -> anyhow::Result<()> { localstore, ln_backends.clone(), supported_units, + HashMap::new(), ) .await?; diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets.rs index 3c9642be3..34e43faac 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets.rs @@ -1,5 +1,6 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use bitcoin::bip32::DerivationPath; use tracing::instrument; use crate::Error; @@ -89,8 +90,14 @@ impl Mint { derivation_path_index: u32, max_order: u8, input_fee_ppk: u64, + custom_paths: HashMap, ) -> Result<(), Error> { - let derivation_path = derivation_path_from_unit(unit, derivation_path_index); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit, derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; + let (keyset, keyset_info) = create_new_keyset( &self.secp_ctx, self.xpriv, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 0699cf7f7..7c7260599 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -54,6 +54,7 @@ pub struct Mint { impl Mint { /// Create new [`Mint`] + #[allow(clippy::too_many_arguments)] pub async fn new( mint_url: &str, seed: &[u8], @@ -63,6 +64,7 @@ impl Mint { ln: HashMap + Send + Sync>>, // Hashmap where the key is the unit and value is (input fee ppk, max_order) supported_units: HashMap, + custom_paths: HashMap, ) -> Result { let secp_ctx = Secp256k1::new(); let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); @@ -125,7 +127,11 @@ impl Mint { highest_index_keyset.derivation_path_index.unwrap_or(0) + 1 }; - let derivation_path = derivation_path_from_unit(unit, derivation_path_index); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit, derivation_path_index) + .ok_or(Error::UnsupportedUnit)?, + }; let (keyset, keyset_info) = create_new_keyset( &secp_ctx, @@ -148,7 +154,10 @@ impl Mint { for (unit, (fee, max_order)) in supported_units { if !active_keyset_units.contains(&unit) { - let derivation_path = derivation_path_from_unit(unit, 0); + let derivation_path = match custom_paths.get(&unit) { + Some(path) => path.clone(), + None => derivation_path_from_unit(unit, 0).ok_or(Error::UnsupportedUnit)?, + }; let (keyset, keyset_info) = create_new_keyset( &secp_ctx, @@ -571,12 +580,17 @@ fn create_new_keyset( (keyset, keyset_info) } -fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> DerivationPath { - DerivationPath::from(vec![ +fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option { + let unit_index = match unit.derivation_index() { + Some(index) => index, + None => return None, + }; + + Some(DerivationPath::from(vec![ ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), - ChildNumber::from_hardened_idx(unit.derivation_index()).expect("0 is a valid index"), + ChildNumber::from_hardened_idx(unit_index).expect("0 is a valid index"), ChildNumber::from_hardened_idx(index).expect("0 is a valid index"), - ]) + ])) } #[cfg(test)] @@ -598,7 +612,7 @@ mod tests { seed, 2, CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0), + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), ); assert_eq!(keyset.unit, CurrencyUnit::Sat); @@ -642,7 +656,7 @@ mod tests { xpriv, 2, CurrencyUnit::Sat, - derivation_path_from_unit(CurrencyUnit::Sat, 0), + derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), ); assert_eq!(keyset.unit, CurrencyUnit::Sat); @@ -722,6 +736,7 @@ mod tests { localstore, HashMap::new(), config.supported_units, + HashMap::new(), ) .await } @@ -777,7 +792,8 @@ mod tests { assert!(keysets.keysets.is_empty()); // generate the first keyset and set it to active - mint.rotate_keyset(CurrencyUnit::default(), 0, 1, 1).await?; + mint.rotate_keyset(CurrencyUnit::default(), 0, 1, 1, HashMap::new()) + .await?; let keysets = mint.keysets().await.unwrap(); assert!(keysets.keysets.len().eq(&1)); @@ -785,7 +801,8 @@ mod tests { let first_keyset_id = keysets.keysets[0].id; // set the first keyset to inactive and generate a new keyset - mint.rotate_keyset(CurrencyUnit::default(), 1, 1, 1).await?; + mint.rotate_keyset(CurrencyUnit::default(), 1, 1, 1, HashMap::new()) + .await?; let keysets = mint.keysets().await.unwrap(); diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index d6c3dffdb..5810cf1f3 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -373,17 +373,20 @@ pub enum CurrencyUnit { Usd, /// Euro Eur, + /// Custom currency unit (3 characters) + Custom([u8; 3]), } #[cfg(feature = "mint")] impl CurrencyUnit { /// Derivation index mint will use for unit - pub fn derivation_index(&self) -> u32 { + pub fn derivation_index(&self) -> Option { match self { - Self::Sat => 0, - Self::Msat => 1, - Self::Usd => 2, - Self::Eur => 3, + Self::Sat => Some(0), + Self::Msat => Some(1), + Self::Usd => Some(2), + Self::Eur => Some(3), + _ => None, } } } @@ -391,12 +394,29 @@ impl CurrencyUnit { impl FromStr for CurrencyUnit { type Err = Error; fn from_str(value: &str) -> Result { - match value { - "sat" => Ok(Self::Sat), - "msat" => Ok(Self::Msat), - "usd" => Ok(Self::Usd), - "eur" => Ok(Self::Eur), - _ => Err(Error::UnsupportedUnit), + let value = &value.to_uppercase(); + match value.as_str() { + "SAT" => Ok(Self::Sat), + "MSAT" => Ok(Self::Msat), + "USD" => Ok(Self::Usd), + "EUR" => Ok(Self::Eur), + c => { + if c.len() != 3 { + return Err(Error::UnsupportedUnit); + } + // Convert to ASCII uppercase bytes + let bytes: [u8; 3] = c + .as_bytes() + .try_into() + .map_err(|_| Error::UnsupportedUnit)?; + + // Validate that all characters are ASCII uppercase letters + if bytes.iter().any(|&b| !b.is_ascii_uppercase()) { + return Err(Error::UnsupportedUnit); + } + + Ok(Self::Custom(bytes)) + } } } } @@ -404,10 +424,14 @@ impl FromStr for CurrencyUnit { impl fmt::Display for CurrencyUnit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - CurrencyUnit::Sat => "sat", - CurrencyUnit::Msat => "msat", - CurrencyUnit::Usd => "usd", - CurrencyUnit::Eur => "eur", + CurrencyUnit::Sat => "USD", + CurrencyUnit::Msat => "MSAT", + CurrencyUnit::Usd => "USD", + CurrencyUnit::Eur => "EUR", + CurrencyUnit::Custom(code) => { + let s = std::str::from_utf8(code).unwrap(); + s + } }; if let Some(width) = f.width() { write!(f, "{:width$}", s, width = width)