|
@@ -0,0 +1,255 @@
|
|
|
|
|
+//! FFI bindings for the NpubCash client SDK
|
|
|
|
|
+//!
|
|
|
|
|
+//! This module provides FFI-compatible bindings for interacting with the NpubCash API.
|
|
|
|
|
+//! The client can be used standalone without requiring a wallet.
|
|
|
|
|
+
|
|
|
|
|
+use std::sync::Arc;
|
|
|
|
|
+
|
|
|
|
|
+use cdk_npubcash::{JwtAuthProvider, NpubCashClient as CdkNpubCashClient};
|
|
|
|
|
+
|
|
|
|
|
+use crate::error::FfiError;
|
|
|
|
|
+use crate::types::MintQuote;
|
|
|
|
|
+
|
|
|
|
|
+/// FFI-compatible NpubCash client
|
|
|
|
|
+///
|
|
|
|
|
+/// This client provides access to the NpubCash API for fetching quotes
|
|
|
|
|
+/// and managing user settings.
|
|
|
|
|
+#[derive(uniffi::Object)]
|
|
|
|
|
+pub struct NpubCashClient {
|
|
|
|
|
+ inner: Arc<CdkNpubCashClient>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[uniffi::export(async_runtime = "tokio")]
|
|
|
|
|
+impl NpubCashClient {
|
|
|
|
|
+ /// Create a new NpubCash client
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Arguments
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// * `base_url` - Base URL of the NpubCash service (e.g., "https://npub.cash")
|
|
|
|
|
+ /// * `nostr_secret_key` - Nostr secret key for authentication. Accepts either:
|
|
|
|
|
+ /// - Hex-encoded secret key (64 characters)
|
|
|
|
|
+ /// - Bech32 `nsec` format (e.g., "nsec1...")
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Errors
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Returns an error if the secret key is invalid or cannot be parsed
|
|
|
|
|
+ #[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),
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Fetch quotes from NpubCash
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Arguments
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// * `since` - Optional Unix timestamp to fetch quotes from. If `None`, fetches all quotes.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Returns
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// A list of quotes from the NpubCash service. The client automatically handles
|
|
|
|
|
+ /// pagination to fetch all available quotes.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Errors
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Returns an error if the API request fails or authentication fails
|
|
|
|
|
+ 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())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Set the mint URL for the user on the NpubCash server
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Updates the default mint URL used by the NpubCash server when creating quotes.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Arguments
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// * `mint_url` - URL of the Cashu mint to use (e.g., "https://mint.example.com")
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// # Errors
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Returns an error if the API request fails or authentication fails
|
|
|
|
|
+ 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())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// A quote from the NpubCash service
|
|
|
|
|
+#[derive(Debug, Clone, uniffi::Record)]
|
|
|
|
|
+pub struct NpubCashQuote {
|
|
|
|
|
+ /// Unique identifier for the quote
|
|
|
|
|
+ pub id: String,
|
|
|
|
|
+ /// Amount in the specified unit
|
|
|
|
|
+ pub amount: u64,
|
|
|
|
|
+ /// Currency or unit for the amount (e.g., "sat")
|
|
|
|
|
+ pub unit: String,
|
|
|
|
|
+ /// Unix timestamp when the quote was created
|
|
|
|
|
+ pub created_at: u64,
|
|
|
|
|
+ /// Unix timestamp when the quote was paid (if paid)
|
|
|
|
|
+ pub paid_at: Option<u64>,
|
|
|
|
|
+ /// Unix timestamp when the quote expires
|
|
|
|
|
+ pub expires_at: Option<u64>,
|
|
|
|
|
+ /// Mint URL associated with the quote
|
|
|
|
|
+ pub mint_url: Option<String>,
|
|
|
|
|
+ /// Lightning invoice request
|
|
|
|
|
+ pub request: Option<String>,
|
|
|
|
|
+ /// Quote state (e.g., "PAID", "PENDING")
|
|
|
|
|
+ pub state: Option<String>,
|
|
|
|
|
+ /// Whether the quote is locked
|
|
|
|
|
+ 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,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Convert a NpubCash quote to a wallet MintQuote
|
|
|
|
|
+///
|
|
|
|
|
+/// This allows the quote to be used with the wallet's minting functions.
|
|
|
|
|
+/// Note that the resulting MintQuote will not have a secret key set,
|
|
|
|
|
+/// which may be required for locked quotes.
|
|
|
|
|
+///
|
|
|
|
|
+/// # Arguments
|
|
|
|
|
+///
|
|
|
|
|
+/// * `quote` - The NpubCash quote to convert
|
|
|
|
|
+///
|
|
|
|
|
+/// # Returns
|
|
|
|
|
+///
|
|
|
|
|
+/// A MintQuote that can be used with wallet minting functions
|
|
|
|
|
+#[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()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Response from updating user settings on NpubCash
|
|
|
|
|
+#[derive(Debug, Clone, uniffi::Record)]
|
|
|
|
|
+pub struct NpubCashUserResponse {
|
|
|
|
|
+ /// Whether the request resulted in an error
|
|
|
|
|
+ pub error: bool,
|
|
|
|
|
+ /// User's public key
|
|
|
|
|
+ pub pubkey: String,
|
|
|
|
|
+ /// Configured mint URL
|
|
|
|
|
+ pub mint_url: Option<String>,
|
|
|
|
|
+ /// Whether quotes are locked
|
|
|
|
|
+ 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,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Derive Nostr keys from a wallet seed
|
|
|
|
|
+///
|
|
|
|
|
+/// This function derives the same Nostr keys that a wallet would use for NpubCash
|
|
|
|
|
+/// authentication. It takes the first 32 bytes of the seed as the secret key.
|
|
|
|
|
+///
|
|
|
|
|
+/// # Arguments
|
|
|
|
|
+///
|
|
|
|
|
+/// * `seed` - The wallet seed bytes (must be at least 32 bytes)
|
|
|
|
|
+///
|
|
|
|
|
+/// # Returns
|
|
|
|
|
+///
|
|
|
|
|
+/// The hex-encoded Nostr secret key that can be used with `NpubCashClient::new()`
|
|
|
|
|
+///
|
|
|
|
|
+/// # Errors
|
|
|
|
|
+///
|
|
|
|
|
+/// Returns an error if the seed is too short or key derivation fails
|
|
|
|
|
+#[uniffi::export]
|
|
|
|
|
+pub fn npubcash_derive_secret_key_from_seed(seed: Vec<u8>) -> Result<String, FfiError> {
|
|
|
|
|
+ if seed.len() < 32 {
|
|
|
|
|
+ return Err(FfiError::internal(
|
|
|
|
|
+ "Seed must be at least 32 bytes".to_string(),
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Use the first 32 bytes of the seed as the secret key
|
|
|
|
|
+ let secret_key = nostr_sdk::SecretKey::from_slice(&seed[..32])
|
|
|
|
|
+ .map_err(|e| FfiError::internal(format!("Failed to derive secret key: {}", e)))?;
|
|
|
|
|
+
|
|
|
|
|
+ Ok(secret_key.to_secret_hex())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Get the public key for a given Nostr secret key
|
|
|
|
|
+///
|
|
|
|
|
+/// # Arguments
|
|
|
|
|
+///
|
|
|
|
|
+/// * `nostr_secret_key` - Nostr secret key. Accepts either:
|
|
|
|
|
+/// - Hex-encoded secret key (64 characters)
|
|
|
|
|
+/// - Bech32 `nsec` format (e.g., "nsec1...")
|
|
|
|
|
+///
|
|
|
|
|
+/// # Returns
|
|
|
|
|
+///
|
|
|
|
|
+/// The hex-encoded public key
|
|
|
|
|
+///
|
|
|
|
|
+/// # Errors
|
|
|
|
|
+///
|
|
|
|
|
+/// Returns an error if the secret key is invalid
|
|
|
|
|
+#[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())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Parse a Nostr secret key from either hex or nsec format
|
|
|
|
|
+fn parse_nostr_secret_key(key: &str) -> Result<nostr_sdk::Keys, FfiError> {
|
|
|
|
|
+ // Try parsing as nsec (bech32) first
|
|
|
|
|
+ if key.starts_with("nsec") {
|
|
|
|
|
+ nostr_sdk::Keys::parse(key)
|
|
|
|
|
+ .map_err(|e| FfiError::internal(format!("Invalid nsec key: {}", e)))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Try parsing as hex
|
|
|
|
|
+ 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))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|