Bläddra i källkod

feat(ffi): npubx.cash (#1570)

asmo 2 dagar sedan
förälder
incheckning
3a1d7ea79a
4 ändrade filer med 302 tillägg och 37 borttagningar
  1. 38 36
      Cargo.lock
  2. 5 1
      crates/cdk-ffi/Cargo.toml
  3. 4 0
      crates/cdk-ffi/src/lib.rs
  4. 255 0
      crates/cdk-ffi/src/npubcash.rs

+ 38 - 36
Cargo.lock

@@ -534,9 +534,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.15.3"
+version = "1.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86"
+checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -544,9 +544,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.36.0"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8"
+checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
 dependencies = [
  "cc",
  "cmake",
@@ -1160,9 +1160,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.53"
+version = "1.2.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
+checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -1354,11 +1354,13 @@ dependencies = [
  "bip39",
  "cdk",
  "cdk-common",
+ "cdk-npubcash",
  "cdk-postgres",
  "cdk-sql-common",
  "cdk-sqlite",
  "futures",
  "log",
+ "nostr-sdk",
  "once_cell",
  "rand 0.9.2",
  "serde",
@@ -3618,7 +3620,7 @@ dependencies = [
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio",
  "tower-service",
  "tracing",
@@ -4041,9 +4043,9 @@ dependencies = [
 
 [[package]]
 name = "libm"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
 
 [[package]]
 name = "libredox"
@@ -4492,9 +4494,9 @@ dependencies = [
 
 [[package]]
 name = "moka"
-version = "0.12.12"
+version = "0.12.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a"
+checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
 dependencies = [
  "async-lock",
  "crossbeam-channel",
@@ -4679,9 +4681,9 @@ dependencies = [
 
 [[package]]
 name = "num-conv"
-version = "0.1.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
 
 [[package]]
 name = "num-integer"
@@ -5636,7 +5638,7 @@ dependencies = [
  "quinn-udp",
  "rustc-hash",
  "rustls 0.23.36",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "thiserror 2.0.18",
  "tokio",
  "tracing",
@@ -5673,16 +5675,16 @@ dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tracing",
  "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.43"
+version = "1.0.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
 dependencies = [
  "proc-macro2",
 ]
@@ -6763,9 +6765,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.6.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
 dependencies = [
  "libc",
  "windows-sys 0.60.2",
@@ -7104,9 +7106,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.45"
+version = "0.3.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
 dependencies = [
  "deranged",
  "itoa",
@@ -7119,15 +7121,15 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
 
 [[package]]
 name = "time-macros"
-version = "0.2.25"
+version = "0.2.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
 dependencies = [
  "num-conv",
  "time-core",
@@ -7245,7 +7247,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio-macros",
  "windows-sys 0.61.2",
 ]
@@ -7301,7 +7303,7 @@ dependencies = [
  "postgres-protocol",
  "postgres-types",
  "rand 0.9.2",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio",
  "tokio-util",
  "whoami",
@@ -7547,7 +7549,7 @@ dependencies = [
  "hyper-util",
  "percent-encoding",
  "pin-project",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "sync_wrapper 1.0.2",
  "tokio",
  "tokio-rustls 0.26.4",
@@ -8884,9 +8886,9 @@ dependencies = [
 
 [[package]]
 name = "uuid"
-version = "1.19.0"
+version = "1.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
+checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
 dependencies = [
  "getrandom 0.3.4",
  "js-sys",
@@ -9678,18 +9680,18 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.33"
+version = "0.8.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
+checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.33"
+version = "0.8.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
+checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9792,9 +9794,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
 
 [[package]]
 name = "zmij"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
+checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
 
 [[package]]
 name = "zopfli"

+ 5 - 1
crates/cdk-ffi/Cargo.toml

@@ -18,6 +18,8 @@ bip39 = { workspace = true }
 cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353", "nostr"] }
 cdk-sqlite = { workspace = true }
 cdk-postgres = { workspace = true, optional = true }
+cdk-npubcash = { workspace = true, optional = true }
+nostr-sdk = { workspace = true, optional = true }
 futures = { workspace = true }
 once_cell = { workspace = true }
 rand = { workspace = true }
@@ -39,9 +41,11 @@ log = "0.4"
 
 
 [features]
-default = ["postgres"]
+default = ["postgres", "npubcash"]
 # Enable Postgres-backed wallet database support in FFI
 postgres = ["cdk-postgres"]
+# Enable NpubCash client bindings
+npubcash = ["cdk-npubcash", "nostr-sdk"]
 
 [dev-dependencies]
 

+ 4 - 0
crates/cdk-ffi/src/lib.rs

@@ -10,6 +10,8 @@ pub mod database;
 pub mod error;
 pub mod logging;
 pub mod multi_mint_wallet;
+#[cfg(feature = "npubcash")]
+pub mod npubcash;
 #[cfg(feature = "postgres")]
 pub mod postgres;
 pub mod sqlite;
@@ -21,6 +23,8 @@ pub use database::*;
 pub use error::*;
 pub use logging::*;
 pub use multi_mint_wallet::*;
+#[cfg(feature = "npubcash")]
+pub use npubcash::*;
 pub use types::*;
 pub use wallet::*;
 

+ 255 - 0
crates/cdk-ffi/src/npubcash.rs

@@ -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))
+    }
+}