diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 9f7efdae..dd29a159 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -388,9 +388,11 @@ pub enum Error { NUT20(#[from] crate::nuts::nut20::Error), /// NUT21 Error #[error(transparent)] + #[cfg(feature = "auth")] NUT21(#[from] crate::nuts::nut21::Error), /// NUT22 Error #[error(transparent)] + #[cfg(feature = "auth")] NUT22(#[from] crate::nuts::nut22::Error), /// NUT23 Error #[error(transparent)] diff --git a/crates/cdk-ffi/src/types.rs b/crates/cdk-ffi/src/types.rs index 60194aa6..9d4b0448 100644 --- a/crates/cdk-ffi/src/types.rs +++ b/crates/cdk-ffi/src/types.rs @@ -1467,9 +1467,314 @@ impl From for cdk::nuts::nut06::SupportedSettings { } } -/// FFI-compatible Nuts settings (simplified - only includes basic boolean flags) +// ----------------------------- +// NUT-04/05 FFI Types +// ----------------------------- + +/// FFI-compatible MintMethodSettings (NUT-04) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct MintMethodSettings { + pub method: PaymentMethod, + pub unit: CurrencyUnit, + pub min_amount: Option, + pub max_amount: Option, + /// For bolt11, whether mint supports setting invoice description + pub description: Option, +} + +impl From for MintMethodSettings { + fn from(s: cdk::nuts::nut04::MintMethodSettings) -> Self { + let description = match s.options { + Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) => Some(description), + _ => None, + }; + Self { + method: s.method.into(), + unit: s.unit.into(), + min_amount: s.min_amount.map(Into::into), + max_amount: s.max_amount.map(Into::into), + description, + } + } +} + +impl TryFrom for cdk::nuts::nut04::MintMethodSettings { + type Error = FfiError; + + fn try_from(s: MintMethodSettings) -> Result { + let options = match (s.method.clone(), s.description) { + (PaymentMethod::Bolt11, Some(description)) => { + Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) + } + _ => None, + }; + Ok(Self { + method: s.method.into(), + unit: s.unit.into(), + min_amount: s.min_amount.map(Into::into), + max_amount: s.max_amount.map(Into::into), + options, + }) + } +} + +/// FFI-compatible Nut04 Settings +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct Nut04Settings { + pub methods: Vec, + pub disabled: bool, +} + +impl From for Nut04Settings { + fn from(s: cdk::nuts::nut04::Settings) -> Self { + Self { + methods: s.methods.into_iter().map(Into::into).collect(), + disabled: s.disabled, + } + } +} + +impl TryFrom for cdk::nuts::nut04::Settings { + type Error = FfiError; + + fn try_from(s: Nut04Settings) -> Result { + Ok(Self { + methods: s + .methods + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + disabled: s.disabled, + }) + } +} + +/// FFI-compatible MeltMethodSettings (NUT-05) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct MeltMethodSettings { + pub method: PaymentMethod, + pub unit: CurrencyUnit, + pub min_amount: Option, + pub max_amount: Option, + /// For bolt11, whether mint supports amountless invoices + pub amountless: Option, +} + +impl From for MeltMethodSettings { + fn from(s: cdk::nuts::nut05::MeltMethodSettings) -> Self { + let amountless = match s.options { + Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) => Some(amountless), + _ => None, + }; + Self { + method: s.method.into(), + unit: s.unit.into(), + min_amount: s.min_amount.map(Into::into), + max_amount: s.max_amount.map(Into::into), + amountless, + } + } +} + +impl TryFrom for cdk::nuts::nut05::MeltMethodSettings { + type Error = FfiError; + + fn try_from(s: MeltMethodSettings) -> Result { + let options = match (s.method.clone(), s.amountless) { + (PaymentMethod::Bolt11, Some(amountless)) => { + Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) + } + _ => None, + }; + Ok(Self { + method: s.method.into(), + unit: s.unit.into(), + min_amount: s.min_amount.map(Into::into), + max_amount: s.max_amount.map(Into::into), + options, + }) + } +} + +/// FFI-compatible Nut05 Settings +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct Nut05Settings { + pub methods: Vec, + pub disabled: bool, +} + +impl From for Nut05Settings { + fn from(s: cdk::nuts::nut05::Settings) -> Self { + Self { + methods: s.methods.into_iter().map(Into::into).collect(), + disabled: s.disabled, + } + } +} + +impl TryFrom for cdk::nuts::nut05::Settings { + type Error = FfiError; + + fn try_from(s: Nut05Settings) -> Result { + Ok(Self { + methods: s + .methods + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + disabled: s.disabled, + }) + } +} + +/// FFI-compatible ProtectedEndpoint (for auth nuts) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct ProtectedEndpoint { + /// HTTP method (GET, POST, etc.) + pub method: String, + /// Endpoint path + pub path: String, +} + +/// FFI-compatible ClearAuthSettings (NUT-21) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct ClearAuthSettings { + /// OpenID Connect discovery URL + pub openid_discovery: String, + /// OAuth 2.0 client ID + pub client_id: String, + /// Protected endpoints requiring clear authentication + pub protected_endpoints: Vec, +} + +/// FFI-compatible BlindAuthSettings (NUT-22) +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct BlindAuthSettings { + /// Maximum number of blind auth tokens that can be minted per request + pub bat_max_mint: u64, + /// Protected endpoints requiring blind authentication + pub protected_endpoints: Vec, +} + +impl From for ClearAuthSettings { + fn from(settings: cdk::nuts::ClearAuthSettings) -> Self { + Self { + openid_discovery: settings.openid_discovery, + client_id: settings.client_id, + protected_endpoints: settings + .protected_endpoints + .into_iter() + .map(Into::into) + .collect(), + } + } +} + +impl TryFrom for cdk::nuts::ClearAuthSettings { + type Error = FfiError; + + fn try_from(settings: ClearAuthSettings) -> Result { + Ok(Self { + openid_discovery: settings.openid_discovery, + client_id: settings.client_id, + protected_endpoints: settings + .protected_endpoints + .into_iter() + .map(|e| e.try_into()) + .collect::, _>>()?, + }) + } +} + +impl From for BlindAuthSettings { + fn from(settings: cdk::nuts::BlindAuthSettings) -> Self { + Self { + bat_max_mint: settings.bat_max_mint, + protected_endpoints: settings + .protected_endpoints + .into_iter() + .map(Into::into) + .collect(), + } + } +} + +impl TryFrom for cdk::nuts::BlindAuthSettings { + type Error = FfiError; + + fn try_from(settings: BlindAuthSettings) -> Result { + Ok(Self { + bat_max_mint: settings.bat_max_mint, + protected_endpoints: settings + .protected_endpoints + .into_iter() + .map(|e| e.try_into()) + .collect::, _>>()?, + }) + } +} + +impl From for ProtectedEndpoint { + fn from(endpoint: cdk::nuts::ProtectedEndpoint) -> Self { + Self { + method: match endpoint.method { + cdk::nuts::Method::Get => "GET".to_string(), + cdk::nuts::Method::Post => "POST".to_string(), + }, + path: endpoint.path.to_string(), + } + } +} + +impl TryFrom for cdk::nuts::ProtectedEndpoint { + type Error = FfiError; + + fn try_from(endpoint: ProtectedEndpoint) -> Result { + let method = match endpoint.method.as_str() { + "GET" => cdk::nuts::Method::Get, + "POST" => cdk::nuts::Method::Post, + _ => { + return Err(FfiError::Generic { + msg: format!( + "Invalid HTTP method: {}. Only GET and POST are supported", + endpoint.method + ), + }) + } + }; + + // Convert path string to RoutePath by matching against known paths + let route_path = match endpoint.path.as_str() { + "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuoteBolt11, + "/v1/mint/bolt11" => cdk::nuts::RoutePath::MintBolt11, + "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuoteBolt11, + "/v1/melt/bolt11" => cdk::nuts::RoutePath::MeltBolt11, + "/v1/swap" => cdk::nuts::RoutePath::Swap, + "/v1/checkstate" => cdk::nuts::RoutePath::Checkstate, + "/v1/restore" => cdk::nuts::RoutePath::Restore, + "/v1/auth/blind/mint" => cdk::nuts::RoutePath::MintBlindAuth, + "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuoteBolt12, + "/v1/mint/bolt12" => cdk::nuts::RoutePath::MintBolt12, + "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuoteBolt12, + "/v1/melt/bolt12" => cdk::nuts::RoutePath::MeltBolt12, + _ => { + return Err(FfiError::Generic { + msg: format!("Unknown route path: {}", endpoint.path), + }) + } + }; + + Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path)) + } +} + +/// FFI-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Nuts { + /// NUT04 Settings + pub nut04: Nut04Settings, + /// NUT05 Settings + pub nut05: Nut05Settings, /// NUT07 Settings - Token state check pub nut07_supported: bool, /// NUT08 Settings - Lightning fee return @@ -1486,6 +1791,10 @@ pub struct Nuts { pub nut14_supported: bool, /// NUT20 Settings - Web sockets pub nut20_supported: bool, + /// NUT21 Settings - Clear authentication + pub nut21: Option, + /// NUT22 Settings - Blind authentication + pub nut22: Option, /// Supported currency units for minting pub mint_units: Vec, /// Supported currency units for melting @@ -1494,7 +1803,20 @@ pub struct Nuts { impl From for Nuts { fn from(nuts: cdk::nuts::Nuts) -> Self { + let mint_units = nuts + .supported_mint_units() + .into_iter() + .map(|u| u.clone().into()) + .collect(); + let melt_units = nuts + .supported_melt_units() + .into_iter() + .map(|u| u.clone().into()) + .collect(); + Self { + nut04: nuts.nut04.clone().into(), + nut05: nuts.nut05.clone().into(), nut07_supported: nuts.nut07.supported, nut08_supported: nuts.nut08.supported, nut09_supported: nuts.nut09.supported, @@ -1503,20 +1825,54 @@ impl From for Nuts { nut12_supported: nuts.nut12.supported, nut14_supported: nuts.nut14.supported, nut20_supported: nuts.nut20.supported, - mint_units: nuts - .supported_mint_units() - .into_iter() - .map(|u| u.clone().into()) - .collect(), - melt_units: nuts - .supported_melt_units() - .into_iter() - .map(|u| u.clone().into()) - .collect(), + nut21: nuts.nut21.map(Into::into), + nut22: nuts.nut22.map(Into::into), + mint_units, + melt_units, } } } +impl TryFrom for cdk::nuts::Nuts { + type Error = FfiError; + + fn try_from(n: Nuts) -> Result { + Ok(Self { + nut04: n.nut04.try_into()?, + nut05: n.nut05.try_into()?, + nut07: cdk::nuts::nut06::SupportedSettings { + supported: n.nut07_supported, + }, + nut08: cdk::nuts::nut06::SupportedSettings { + supported: n.nut08_supported, + }, + nut09: cdk::nuts::nut06::SupportedSettings { + supported: n.nut09_supported, + }, + nut10: cdk::nuts::nut06::SupportedSettings { + supported: n.nut10_supported, + }, + nut11: cdk::nuts::nut06::SupportedSettings { + supported: n.nut11_supported, + }, + nut12: cdk::nuts::nut06::SupportedSettings { + supported: n.nut12_supported, + }, + nut14: cdk::nuts::nut06::SupportedSettings { + supported: n.nut14_supported, + }, + nut15: Default::default(), + nut17: Default::default(), + nut19: Default::default(), + nut20: cdk::nuts::nut06::SupportedSettings { + supported: n.nut20_supported, + }, + nut21: n.nut21.map(|s| s.try_into()).transpose()?, + nut22: n.nut22.map(|s| s.try_into()).transpose()?, + }) + } +} + impl Nuts { /// Convert Nuts to JSON string pub fn to_json(&self) -> Result { @@ -1588,6 +1944,8 @@ impl From for MintInfo { impl From for cdk::nuts::MintInfo { fn from(info: MintInfo) -> Self { + // Convert FFI Nuts back to cdk::nuts::Nuts (best-effort) + let nuts_cdk: cdk::nuts::Nuts = info.nuts.clone().try_into().unwrap_or_default(); Self { name: info.name, pubkey: info.pubkey.and_then(|p| p.parse().ok()), @@ -1597,7 +1955,7 @@ impl From for cdk::nuts::MintInfo { contact: info .contact .map(|contacts| contacts.into_iter().map(Into::into).collect()), - nuts: cdk::nuts::Nuts::default(), // Simplified conversion + nuts: nuts_cdk, icon_url: info.icon_url, urls: info.urls, motd: info.motd,