add FFI types for NUT-04 and NUT-05 (#1101)

* feat: add FFI types and conversion logic for NUT-04 and NUT-05 settings in `cdk-ffi`

* feat: remove auth feature and deafult to having auth

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
asmo
2025-09-23 12:44:11 +02:00
committed by GitHub
parent fdd99e1afc
commit 1e6102f494
2 changed files with 372 additions and 12 deletions

View File

@@ -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)]

View File

@@ -1467,9 +1467,314 @@ impl From<SupportedSettings> 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<Amount>,
pub max_amount: Option<Amount>,
/// For bolt11, whether mint supports setting invoice description
pub description: Option<bool>,
}
impl From<cdk::nuts::nut04::MintMethodSettings> 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<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
type Error = FfiError;
fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
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<MintMethodSettings>,
pub disabled: bool,
}
impl From<cdk::nuts::nut04::Settings> 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<Nut04Settings> for cdk::nuts::nut04::Settings {
type Error = FfiError;
fn try_from(s: Nut04Settings) -> Result<Self, Self::Error> {
Ok(Self {
methods: s
.methods
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
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<Amount>,
pub max_amount: Option<Amount>,
/// For bolt11, whether mint supports amountless invoices
pub amountless: Option<bool>,
}
impl From<cdk::nuts::nut05::MeltMethodSettings> 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<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
type Error = FfiError;
fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
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<MeltMethodSettings>,
pub disabled: bool,
}
impl From<cdk::nuts::nut05::Settings> 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<Nut05Settings> for cdk::nuts::nut05::Settings {
type Error = FfiError;
fn try_from(s: Nut05Settings) -> Result<Self, Self::Error> {
Ok(Self {
methods: s
.methods
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
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<ProtectedEndpoint>,
}
/// 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<ProtectedEndpoint>,
}
impl From<cdk::nuts::ClearAuthSettings> 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<ClearAuthSettings> for cdk::nuts::ClearAuthSettings {
type Error = FfiError;
fn try_from(settings: ClearAuthSettings) -> Result<Self, Self::Error> {
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::<Result<Vec<_>, _>>()?,
})
}
}
impl From<cdk::nuts::BlindAuthSettings> 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<BlindAuthSettings> for cdk::nuts::BlindAuthSettings {
type Error = FfiError;
fn try_from(settings: BlindAuthSettings) -> Result<Self, Self::Error> {
Ok(Self {
bat_max_mint: settings.bat_max_mint,
protected_endpoints: settings
.protected_endpoints
.into_iter()
.map(|e| e.try_into())
.collect::<Result<Vec<_>, _>>()?,
})
}
}
impl From<cdk::nuts::ProtectedEndpoint> 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<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
type Error = FfiError;
fn try_from(endpoint: ProtectedEndpoint) -> Result<Self, Self::Error> {
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<ClearAuthSettings>,
/// NUT22 Settings - Blind authentication
pub nut22: Option<BlindAuthSettings>,
/// Supported currency units for minting
pub mint_units: Vec<CurrencyUnit>,
/// Supported currency units for melting
@@ -1494,7 +1803,20 @@ pub struct Nuts {
impl From<cdk::nuts::Nuts> 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<cdk::nuts::Nuts> 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<Nuts> for cdk::nuts::Nuts {
type Error = FfiError;
fn try_from(n: Nuts) -> Result<Self, Self::Error> {
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<String, FfiError> {
@@ -1588,6 +1944,8 @@ impl From<cdk::nuts::MintInfo> for MintInfo {
impl From<MintInfo> 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<MintInfo> 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,