use std::sync::Arc;
use cdk_npubcash::{JwtAuthProvider, NpubCashClient as CdkNpubCashClient};
use crate::error::FfiError;
use crate::types::MintQuote;
#[derive(uniffi::Object)]
pub struct NpubCashClient {
inner: Arc<CdkNpubCashClient>,
}
#[uniffi::export(async_runtime = "tokio")]
impl NpubCashClient {
#[uniffi::constructor]
pub fn new(base_url: String, nostr_secret_key: String) -> Result<Self, FfiError> {
let keys = parse_nostr_secret_key(&nostr_secret_key)?;
let auth_provider = Arc::new(JwtAuthProvider::new(base_url.clone(), keys));
let client = CdkNpubCashClient::new(base_url, auth_provider);
Ok(Self {
inner: Arc::new(client),
})
}
pub async fn get_quotes(&self, since: Option<u64>) -> Result<Vec<NpubCashQuote>, FfiError> {
let quotes = self
.inner
.get_quotes(since)
.await
.map_err(|e| FfiError::internal(e.to_string()))?;
Ok(quotes.into_iter().map(Into::into).collect())
}
pub async fn set_mint_url(&self, mint_url: String) -> Result<NpubCashUserResponse, FfiError> {
let response = self
.inner
.set_mint_url(mint_url)
.await
.map_err(|e| FfiError::internal(e.to_string()))?;
Ok(response.into())
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct NpubCashQuote {
pub id: String,
pub amount: u64,
pub unit: String,
pub created_at: u64,
pub paid_at: Option<u64>,
pub expires_at: Option<u64>,
pub mint_url: Option<String>,
pub request: Option<String>,
pub state: Option<String>,
pub locked: Option<bool>,
}
impl From<cdk_npubcash::Quote> for NpubCashQuote {
fn from(quote: cdk_npubcash::Quote) -> Self {
Self {
id: quote.id,
amount: quote.amount,
unit: quote.unit,
created_at: quote.created_at,
paid_at: quote.paid_at,
expires_at: quote.expires_at,
mint_url: quote.mint_url,
request: quote.request,
state: quote.state,
locked: quote.locked,
}
}
}
#[uniffi::export]
pub fn npubcash_quote_to_mint_quote(quote: NpubCashQuote) -> MintQuote {
let cdk_quote = cdk_npubcash::Quote {
id: quote.id,
amount: quote.amount,
unit: quote.unit,
created_at: quote.created_at,
paid_at: quote.paid_at,
expires_at: quote.expires_at,
mint_url: quote.mint_url,
request: quote.request,
state: quote.state,
locked: quote.locked,
};
let mint_quote: cdk::wallet::MintQuote = cdk_quote.into();
mint_quote.into()
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct NpubCashUserResponse {
pub error: bool,
pub pubkey: String,
pub mint_url: Option<String>,
pub lock_quote: bool,
}
impl From<cdk_npubcash::UserResponse> for NpubCashUserResponse {
fn from(response: cdk_npubcash::UserResponse) -> Self {
Self {
error: response.error,
pubkey: response.data.user.pubkey,
mint_url: response.data.user.mint_url,
lock_quote: response.data.user.lock_quote,
}
}
}
#[uniffi::export]
pub fn npubcash_derive_secret_key_from_seed(seed: Vec<u8>) -> Result<String, FfiError> {
if seed.len() < 64 {
return Err(FfiError::internal(
"Seed must be at least 64 bytes".to_string(),
));
}
let seed: [u8; 64] = seed[..64]
.try_into()
.map_err(|_| FfiError::internal("Failed to read wallet seed bytes".to_string()))?;
let secret_key = cdk::wallet::derive_npubcash_secret_key_from_seed(&seed)
.map_err(|e| FfiError::internal(format!("Failed to derive secret key: {}", e)))?;
Ok(secret_key.to_secret_hex())
}
#[uniffi::export]
pub fn npubcash_get_pubkey(nostr_secret_key: String) -> Result<String, FfiError> {
let keys = parse_nostr_secret_key(&nostr_secret_key)?;
Ok(keys.public_key().to_hex())
}
fn parse_nostr_secret_key(key: &str) -> Result<nostr_sdk::Keys, FfiError> {
if key.starts_with("nsec") {
nostr_sdk::Keys::parse(key)
.map_err(|e| FfiError::internal(format!("Invalid nsec key: {}", e)))
} else {
let secret_key = nostr_sdk::SecretKey::parse(key)
.map_err(|e| FfiError::internal(format!("Invalid hex secret key: {}", e)))?;
Ok(nostr_sdk::Keys::new(secret_key))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn npubcash_seed_derivation_uses_wallet_nip06_key() {
let seed = [0x42u8; 64];
let secret_hex = npubcash_derive_secret_key_from_seed(seed.to_vec())
.expect("npubcash key derives from wallet seed");
let wallet_secret = cdk::wallet::derive_npubcash_secret_key_from_seed(&seed)
.expect("wallet npubcash key derives");
assert_eq!(secret_hex, wallet_secret.to_secret_hex());
assert_ne!(&wallet_secret.to_secret_bytes()[..], &seed[..32]);
}
}