Explorar el Código

FFI bindings for Wallet (#932)

* feat: Add initial CDK FFI bindings using UniFFI proc macros

* feat: Add complete Wallet methods to CDK FFI bindings
David Caseria hace 1 mes
padre
commit
21b4080810

+ 6 - 0
.github/workflows/ci.yml

@@ -117,6 +117,12 @@ jobs:
             -p cdk-signatory,
             -p cdk-mint-rpc,
             
+            # FFI bindings
+            -p cdk-ffi,
+            -p cdk-ffi --no-default-features,
+            -p cdk-ffi --no-default-features --features auth,
+            -p cdk-ffi --no-default-features --features bip353,
+            
             # Binaries
             --bin cdk-cli,
             --bin cdk-cli --features sqlcipher,

+ 8 - 0
Cargo.toml

@@ -127,5 +127,13 @@ inherits = "dev"
 incremental = false
 lto = "off"
 
+[profile.release-smaller]
+inherits = "release"
+opt-level = 'z'     # Optimize for size.
+lto = true          # Enable Link Time Optimization
+codegen-units = 1   # Reduce number of codegen units to increase optimizations.
+panic = "abort"     # Abort on panic
+strip = "debuginfo" # Partially strip symbols from binary
+
 [workspace.metadata.crane]
 name = "cdk-workspace"

+ 43 - 0
crates/cdk-ffi/Cargo.toml

@@ -0,0 +1,43 @@
+[package]
+name = "cdk-ffi"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[lib]
+crate-type = ["cdylib", "staticlib"]
+name = "cdk_ffi"
+
+[features]
+default = ["auth", "bip353"]
+auth = ["cdk/auth", "cdk-common/auth", "cashu/auth"]
+bip353 = ["cdk/bip353"]
+
+[dependencies]
+async-trait = { workspace = true }
+bip39 = { workspace = true }
+cashu = { workspace = true }
+cdk = { workspace = true, default-features = false, features = ["wallet"] }
+cdk-common = { workspace = true }
+cdk-sqlite = { workspace = true }
+ctor = "0.2"
+futures = { workspace = true }
+once_cell = { workspace = true }
+rand = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
+uniffi = { version = "0.29", features = ["cli", "tokio"] }
+url = { workspace = true }
+uuid = { workspace = true, features = ["v4"] }
+
+
+[dev-dependencies]
+
+[[bin]]
+name = "uniffi-bindgen"
+path = "src/bin/uniffi-bindgen.rs"
+

+ 61 - 0
crates/cdk-ffi/README.md

@@ -0,0 +1,61 @@
+# CDK FFI Bindings
+
+UniFFI bindings for the CDK (Cashu Development Kit), providing foreign function interface access to wallet functionality for multiple programming languages.
+
+## Supported Languages
+
+- **🐍 Python** - With REPL integration for development
+- **🍎 Swift** - iOS and macOS development
+- **🎯 Kotlin** - Android and JVM development
+
+## Development Tasks
+
+### Build & Check
+```bash
+just ffi-build        # Build FFI library (release)
+just ffi-build --debug # Build debug version
+just ffi-check         # Check compilation
+just ffi-clean         # Clean build artifacts
+```
+
+### Generate Bindings
+```bash
+# Generate for specific languages
+just ffi-generate python
+just ffi-generate swift
+just ffi-generate kotlin
+
+# Generate all languages
+just ffi-generate-all
+
+# Use --debug for faster development builds
+just ffi-generate python --debug
+```
+
+### Development & Testing
+```bash
+# Python development with REPL
+just ffi-dev-python    # Generates bindings and opens Python REPL with cdk_ffi loaded
+
+# Test bindings
+just ffi-test-python   # Test Python bindings import
+```
+
+## Quick Start
+
+```bash
+# Start development
+just ffi-dev-python
+
+# In the Python REPL:
+>>> dir(cdk_ffi)  # Explore available functions
+>>> help(cdk_ffi.generate_mnemonic)  # Get help
+```
+
+## Language Packages
+
+For production use, see language-specific repositories:
+
+- [cdk-swift](https://github.com/cashubtc/cdk-swift) - iOS/macOS packages
+- [cdk-kotlin](https://github.com/cashubtc/cdk-kotlin) - Android/JVM packages  
+- [cdk-python](https://github.com/cashubtc/cdk-python) - PyPI packages

+ 3 - 0
crates/cdk-ffi/src/bin/uniffi-bindgen.rs

@@ -0,0 +1,3 @@
+fn main() {
+    uniffi::uniffi_bindgen_main()
+}

+ 942 - 0
crates/cdk-ffi/src/database.rs

@@ -0,0 +1,942 @@
+//! FFI Database bindings
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cdk_common::database::WalletDatabase as CdkWalletDatabase;
+use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
+
+use crate::error::FfiError;
+use crate::types::*;
+
+/// FFI-compatible trait for wallet database operations
+/// This trait mirrors the CDK WalletDatabase trait but uses FFI-compatible types
+#[uniffi::export(with_foreign)]
+#[async_trait::async_trait]
+pub trait WalletDatabase: Send + Sync {
+    // Mint Management
+    /// Add Mint to storage
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError>;
+
+    /// Remove Mint from storage
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>;
+
+    /// Get mint from storage
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError>;
+
+    /// Get all mints from storage
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError>;
+
+    /// Update mint url
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), FfiError>;
+
+    // Keyset Management
+    /// Add mint keyset to storage
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError>;
+
+    /// Get mint keysets for mint url
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, FfiError>;
+
+    /// Get mint keyset by id
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError>;
+
+    // Mint Quote Management
+    /// Add mint quote to storage
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>;
+
+    /// Get mint quote from storage
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError>;
+
+    /// Get mint quotes from storage
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError>;
+
+    /// Remove mint quote from storage
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>;
+
+    // Melt Quote Management
+    /// Add melt quote to storage
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>;
+
+    /// Get melt quote from storage
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError>;
+
+    /// Get melt quotes from storage
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError>;
+
+    /// Remove melt quote from storage
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>;
+
+    // Keys Management
+    /// Add Keys to storage
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>;
+
+    /// Get Keys from storage
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError>;
+
+    /// Remove Keys from storage
+    async fn remove_keys(&self, id: Id) -> Result<(), FfiError>;
+
+    // Proof Management
+    /// Update the proofs in storage by adding new proofs or removing proofs by their Y value
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError>;
+
+    /// Get proofs from storage
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError>;
+
+    /// Update proofs state in storage
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError>;
+
+    // Keyset Counter Management
+    /// Increment Keyset counter
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError>;
+
+    // Transaction Management
+    /// Add transaction to storage
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError>;
+
+    /// Get transaction from storage
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, FfiError>;
+
+    /// List transactions from storage
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, FfiError>;
+
+    /// Remove transaction from storage
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>;
+}
+
+/// Internal bridge trait to convert from the FFI trait to the CDK database trait
+/// This allows us to bridge between the UniFFI trait and the CDK's internal database trait
+struct WalletDatabaseBridge {
+    ffi_db: Arc<dyn WalletDatabase>,
+}
+
+impl WalletDatabaseBridge {
+    fn new(ffi_db: Arc<dyn WalletDatabase>) -> Self {
+        Self { ffi_db }
+    }
+}
+
+impl std::fmt::Debug for WalletDatabaseBridge {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "WalletDatabaseBridge")
+    }
+}
+
+#[async_trait::async_trait]
+impl CdkWalletDatabase for WalletDatabaseBridge {
+    type Err = cdk_common::database::Error;
+
+    // Mint Management
+    async fn add_mint(
+        &self,
+        mint_url: cdk_common::mint_url::MintUrl,
+        mint_info: Option<cdk_common::nuts::MintInfo>,
+    ) -> Result<(), Self::Err> {
+        let ffi_mint_url = mint_url.into();
+        let ffi_mint_info = mint_info.map(Into::into);
+
+        self.ffi_db
+            .add_mint(ffi_mint_url, ffi_mint_info)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_mint(&self, mint_url: cdk_common::mint_url::MintUrl) -> Result<(), Self::Err> {
+        let ffi_mint_url = mint_url.into();
+        self.ffi_db
+            .remove_mint(ffi_mint_url)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_mint(
+        &self,
+        mint_url: cdk_common::mint_url::MintUrl,
+    ) -> Result<Option<cdk_common::nuts::MintInfo>, Self::Err> {
+        let ffi_mint_url = mint_url.into();
+        let result = self
+            .ffi_db
+            .get_mint(ffi_mint_url)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mints(
+        &self,
+    ) -> Result<HashMap<cdk_common::mint_url::MintUrl, Option<cdk_common::nuts::MintInfo>>, Self::Err>
+    {
+        let result = self
+            .ffi_db
+            .get_mints()
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+
+        let mut cdk_result = HashMap::new();
+        for (ffi_mint_url, mint_info_opt) in result {
+            let cdk_url = ffi_mint_url.try_into().map_err(|e: FfiError| {
+                cdk_common::database::Error::Database(e.to_string().into())
+            })?;
+            cdk_result.insert(cdk_url, mint_info_opt.map(Into::into));
+        }
+        Ok(cdk_result)
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: cdk_common::mint_url::MintUrl,
+        new_mint_url: cdk_common::mint_url::MintUrl,
+    ) -> Result<(), Self::Err> {
+        let ffi_old_mint_url = old_mint_url.into();
+        let ffi_new_mint_url = new_mint_url.into();
+        self.ffi_db
+            .update_mint_url(ffi_old_mint_url, ffi_new_mint_url)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Keyset Management
+    async fn add_mint_keysets(
+        &self,
+        mint_url: cdk_common::mint_url::MintUrl,
+        keysets: Vec<cdk_common::nuts::KeySetInfo>,
+    ) -> Result<(), Self::Err> {
+        let ffi_mint_url = mint_url.into();
+        let ffi_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
+
+        self.ffi_db
+            .add_mint_keysets(ffi_mint_url, ffi_keysets)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_mint_keysets(
+        &self,
+        mint_url: cdk_common::mint_url::MintUrl,
+    ) -> Result<Option<Vec<cdk_common::nuts::KeySetInfo>>, Self::Err> {
+        let ffi_mint_url = mint_url.into();
+        let result = self
+            .ffi_db
+            .get_mint_keysets(ffi_mint_url)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+    }
+
+    async fn get_keyset_by_id(
+        &self,
+        keyset_id: &cdk_common::nuts::Id,
+    ) -> Result<Option<cdk_common::nuts::KeySetInfo>, Self::Err> {
+        let ffi_id = (*keyset_id).into();
+        let result = self
+            .ffi_db
+            .get_keyset_by_id(ffi_id)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(Into::into))
+    }
+
+    // Mint Quote Management
+    async fn add_mint_quote(&self, quote: cdk_common::wallet::MintQuote) -> Result<(), Self::Err> {
+        let ffi_quote = quote.into();
+        self.ffi_db
+            .add_mint_quote(ffi_quote)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_mint_quote(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<cdk_common::wallet::MintQuote>, Self::Err> {
+        let result = self
+            .ffi_db
+            .get_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into().map_err(|e: FfiError| {
+                    cdk_common::database::Error::Database(e.to_string().into())
+                })
+            })
+            .transpose()?)
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<cdk_common::wallet::MintQuote>, Self::Err> {
+        let result = self
+            .ffi_db
+            .get_mint_quotes()
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .into_iter()
+            .map(|q| {
+                q.try_into().map_err(|e: FfiError| {
+                    cdk_common::database::Error::Database(e.to_string().into())
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
+        self.ffi_db
+            .remove_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Melt Quote Management
+    async fn add_melt_quote(&self, quote: cdk_common::wallet::MeltQuote) -> Result<(), Self::Err> {
+        let ffi_quote = quote.into();
+        self.ffi_db
+            .add_melt_quote(ffi_quote)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_melt_quote(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<cdk_common::wallet::MeltQuote>, Self::Err> {
+        let result = self
+            .ffi_db
+            .get_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into().map_err(|e: FfiError| {
+                    cdk_common::database::Error::Database(e.to_string().into())
+                })
+            })
+            .transpose()?)
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<cdk_common::wallet::MeltQuote>, Self::Err> {
+        let result = self
+            .ffi_db
+            .get_melt_quotes()
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .into_iter()
+            .map(|q| {
+                q.try_into().map_err(|e: FfiError| {
+                    cdk_common::database::Error::Database(e.to_string().into())
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
+        self.ffi_db
+            .remove_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Keys Management
+    async fn add_keys(&self, keyset: cashu::KeySet) -> Result<(), Self::Err> {
+        let ffi_keyset: KeySet = keyset.into();
+        self.ffi_db
+            .add_keys(ffi_keyset)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_keys(
+        &self,
+        id: &cdk_common::nuts::Id,
+    ) -> Result<Option<cdk_common::nuts::Keys>, Self::Err> {
+        let ffi_id: Id = (*id).into();
+        let result = self
+            .ffi_db
+            .get_keys(ffi_id)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+
+        // Convert FFI Keys back to CDK Keys using TryFrom
+        result
+            .map(|ffi_keys| {
+                ffi_keys.try_into().map_err(|e: FfiError| {
+                    cdk_common::database::Error::Database(e.to_string().into())
+                })
+            })
+            .transpose()
+    }
+
+    async fn remove_keys(&self, id: &cdk_common::nuts::Id) -> Result<(), Self::Err> {
+        let ffi_id = (*id).into();
+        self.ffi_db
+            .remove_keys(ffi_id)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Proof Management
+    async fn update_proofs(
+        &self,
+        added: Vec<cdk_common::common::ProofInfo>,
+        removed_ys: Vec<cdk_common::nuts::PublicKey>,
+    ) -> Result<(), Self::Err> {
+        let ffi_added: Vec<ProofInfo> = added.into_iter().map(Into::into).collect();
+        let ffi_removed_ys: Vec<PublicKey> = removed_ys.into_iter().map(Into::into).collect();
+
+        self.ffi_db
+            .update_proofs(ffi_added, ffi_removed_ys)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<cdk_common::mint_url::MintUrl>,
+        unit: Option<cdk_common::nuts::CurrencyUnit>,
+        state: Option<Vec<cdk_common::nuts::State>>,
+        spending_conditions: Option<Vec<cdk_common::nuts::SpendingConditions>>,
+    ) -> Result<Vec<cdk_common::common::ProofInfo>, Self::Err> {
+        let ffi_mint_url = mint_url.map(Into::into);
+        let ffi_unit = unit.map(Into::into);
+        let ffi_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let ffi_spending_conditions =
+            spending_conditions.map(|sc| sc.into_iter().map(Into::into).collect());
+
+        let result = self
+            .ffi_db
+            .get_proofs(ffi_mint_url, ffi_unit, ffi_state, ffi_spending_conditions)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+
+        // Convert back to CDK ProofInfo
+        let cdk_result: Result<Vec<cdk_common::common::ProofInfo>, cdk_common::database::Error> =
+            result
+                .into_iter()
+                .map(|info| {
+                    Ok(cdk_common::common::ProofInfo {
+                        proof: info.proof.inner.clone(),
+                        y: info.y.try_into().map_err(|e: FfiError| {
+                            cdk_common::database::Error::Database(e.to_string().into())
+                        })?,
+                        mint_url: info.mint_url.try_into().map_err(|e: FfiError| {
+                            cdk_common::database::Error::Database(e.to_string().into())
+                        })?,
+                        state: info.state.into(),
+                        spending_condition: info
+                            .spending_condition
+                            .map(|sc| sc.try_into())
+                            .transpose()
+                            .map_err(|e: FfiError| {
+                                cdk_common::database::Error::Database(e.to_string().into())
+                            })?,
+                        unit: info.unit.into(),
+                    })
+                })
+                .collect();
+
+        cdk_result
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<cdk_common::nuts::PublicKey>,
+        state: cdk_common::nuts::State,
+    ) -> Result<(), Self::Err> {
+        let ffi_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
+        let ffi_state = state.into();
+
+        self.ffi_db
+            .update_proofs_state(ffi_ys, ffi_state)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Keyset Counter Management
+    async fn increment_keyset_counter(
+        &self,
+        keyset_id: &cdk_common::nuts::Id,
+        count: u32,
+    ) -> Result<u32, Self::Err> {
+        let ffi_id = (*keyset_id).into();
+        self.ffi_db
+            .increment_keyset_counter(ffi_id, count)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    // Transaction Management
+    async fn add_transaction(
+        &self,
+        transaction: cdk_common::wallet::Transaction,
+    ) -> Result<(), Self::Err> {
+        let ffi_transaction = transaction.into();
+        self.ffi_db
+            .add_transaction(ffi_transaction)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_transaction(
+        &self,
+        transaction_id: cdk_common::wallet::TransactionId,
+    ) -> Result<Option<cdk_common::wallet::Transaction>, Self::Err> {
+        let ffi_id = transaction_id.into();
+        let result = self
+            .ffi_db
+            .get_transaction(ffi_id)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+
+        result
+            .map(|tx| tx.try_into())
+            .transpose()
+            .map_err(|e: FfiError| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn list_transactions(
+        &self,
+        mint_url: Option<cdk_common::mint_url::MintUrl>,
+        direction: Option<cdk_common::wallet::TransactionDirection>,
+        unit: Option<cdk_common::nuts::CurrencyUnit>,
+    ) -> Result<Vec<cdk_common::wallet::Transaction>, Self::Err> {
+        let ffi_mint_url = mint_url.map(Into::into);
+        let ffi_direction = direction.map(Into::into);
+        let ffi_unit = unit.map(Into::into);
+
+        let result = self
+            .ffi_db
+            .list_transactions(ffi_mint_url, ffi_direction, ffi_unit)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))?;
+
+        result
+            .into_iter()
+            .map(|tx| tx.try_into())
+            .collect::<Result<Vec<_>, FfiError>>()
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_transaction(
+        &self,
+        transaction_id: cdk_common::wallet::TransactionId,
+    ) -> Result<(), Self::Err> {
+        let ffi_id = transaction_id.into();
+        self.ffi_db
+            .remove_transaction(ffi_id)
+            .await
+            .map_err(|e| cdk_common::database::Error::Database(e.to_string().into()))
+    }
+}
+
+/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
+#[derive(uniffi::Object)]
+pub struct WalletSqliteDatabase {
+    inner: Arc<CdkWalletSqliteDatabase>,
+}
+
+impl WalletSqliteDatabase {
+    // No additional methods needed beyond the trait implementation
+}
+
+#[uniffi::export]
+impl WalletSqliteDatabase {
+    /// Create a new WalletSqliteDatabase with the given work directory
+    #[uniffi::constructor]
+    pub async fn new(file_path: String) -> Result<Arc<Self>, FfiError> {
+        let db = CdkWalletSqliteDatabase::new(file_path.as_str())
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(Arc::new(Self {
+            inner: Arc::new(db),
+        }))
+    }
+
+    /// Create an in-memory database
+    #[uniffi::constructor]
+    pub async fn new_in_memory() -> Result<Arc<Self>, FfiError> {
+        let db = cdk_sqlite::wallet::memory::empty()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(Arc::new(Self {
+            inner: Arc::new(db),
+        }))
+    }
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+#[async_trait::async_trait]
+impl WalletDatabase for WalletSqliteDatabase {
+    // Mint Management
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let cdk_mint_info = mint_info.map(Into::into);
+        self.inner
+            .add_mint(cdk_mint_url, cdk_mint_info)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        self.inner
+            .remove_mint(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let result = self
+            .inner
+            .get_mint(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
+        let result = self
+            .inner
+            .get_mints()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.map(Into::into)))
+            .collect())
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), FfiError> {
+        let cdk_old_mint_url = old_mint_url.try_into()?;
+        let cdk_new_mint_url = new_mint_url.try_into()?;
+        self.inner
+            .update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Keyset Management
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let cdk_keysets: Vec<cdk_common::nuts::KeySetInfo> =
+            keysets.into_iter().map(Into::into).collect();
+        self.inner
+            .add_mint_keysets(cdk_mint_url, cdk_keysets)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let result = self
+            .inner
+            .get_mint_keysets(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+    }
+
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
+        let cdk_id = keyset_id.into();
+        let result = self
+            .inner
+            .get_keyset_by_id(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    // Mint Quote Management
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
+        let cdk_quote = quote.try_into()?;
+        self.inner
+            .add_mint_quote(cdk_quote)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_mint_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_mint_quotes()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.into_iter().map(|q| q.into()).collect())
+    }
+
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner
+            .remove_mint_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Melt Quote Management
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
+        let cdk_quote = quote.try_into()?;
+        self.inner
+            .add_melt_quote(cdk_quote)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_melt_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_melt_quotes()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.into_iter().map(|q| q.into()).collect())
+    }
+
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner
+            .remove_melt_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Keys Management
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
+        // Convert FFI KeySet to cashu::KeySet
+        let cashu_keyset: cashu::KeySet = keyset.try_into()?;
+        self.inner
+            .add_keys(cashu_keyset)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
+        let cdk_id = id.into();
+        let result = self
+            .inner
+            .get_keys(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
+        let cdk_id = id.into();
+        self.inner
+            .remove_keys(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Proof Management
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError> {
+        // Convert FFI types to CDK types
+        let cdk_added: Result<Vec<cdk_common::common::ProofInfo>, FfiError> = added
+            .into_iter()
+            .map(|info| {
+                Ok::<cdk_common::common::ProofInfo, FfiError>(cdk_common::common::ProofInfo {
+                    proof: info.proof.inner.clone(),
+                    y: info.y.try_into()?,
+                    mint_url: info.mint_url.try_into()?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()?,
+                    unit: info.unit.into(),
+                })
+            })
+            .collect();
+        let cdk_added = cdk_added?;
+
+        let cdk_removed_ys: Result<Vec<cdk_common::nuts::PublicKey>, FfiError> =
+            removed_ys.into_iter().map(|pk| pk.try_into()).collect();
+        let cdk_removed_ys = cdk_removed_ys?;
+
+        self.inner
+            .update_proofs(cdk_added, cdk_removed_ys)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError> {
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_unit = unit.map(Into::into);
+        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let cdk_spending_conditions: Option<Vec<cdk_common::nuts::SpendingConditions>> =
+            spending_conditions
+                .map(|sc| {
+                    sc.into_iter()
+                        .map(|c| c.try_into())
+                        .collect::<Result<Vec<_>, FfiError>>()
+                })
+                .transpose()?;
+
+        let result = self
+            .inner
+            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError> {
+        let cdk_ys: Result<Vec<cdk_common::nuts::PublicKey>, FfiError> =
+            ys.into_iter().map(|pk| pk.try_into()).collect();
+        let cdk_ys = cdk_ys?;
+        let cdk_state = state.into();
+
+        self.inner
+            .update_proofs_state(cdk_ys, cdk_state)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Keyset Counter Management
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
+        let cdk_id = keyset_id.into();
+        self.inner
+            .increment_keyset_counter(&cdk_id, count)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    // Transaction Management
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
+        // Convert FFI Transaction to CDK Transaction using TryFrom
+        let cdk_transaction: cdk_common::wallet::Transaction = transaction.try_into()?;
+
+        self.inner
+            .add_transaction(cdk_transaction)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, FfiError> {
+        let cdk_id = transaction_id.try_into()?;
+        let result = self
+            .inner
+            .get_transaction(cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, FfiError> {
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_direction = direction.map(Into::into);
+        let cdk_unit = unit.map(Into::into);
+
+        let result = self
+            .inner
+            .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
+        let cdk_id = transaction_id.try_into()?;
+        self.inner
+            .remove_transaction(cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+}
+
+/// Helper function to create a CDK database from the FFI trait
+pub fn create_cdk_database_from_ffi(
+    ffi_db: Arc<dyn WalletDatabase>,
+) -> Arc<dyn CdkWalletDatabase<Err = cdk_common::database::Error> + Send + Sync> {
+    Arc::new(WalletDatabaseBridge::new(ffi_db))
+}

+ 124 - 0
crates/cdk-ffi/src/error.rs

@@ -0,0 +1,124 @@
+//! FFI Error types
+
+use cdk::Error as CdkError;
+
+/// FFI Error type that wraps CDK errors for cross-language use
+#[derive(Debug, thiserror::Error, uniffi::Error)]
+#[uniffi(flat_error)]
+pub enum FfiError {
+    /// Generic error with message
+    #[error("CDK Error: {msg}")]
+    Generic { msg: String },
+
+    /// Amount overflow
+    #[error("Amount overflow")]
+    AmountOverflow,
+
+    /// Division by zero
+    #[error("Division by zero")]
+    DivisionByZero,
+
+    /// Amount error
+    #[error("Amount error: {msg}")]
+    Amount { msg: String },
+
+    /// Payment failed
+    #[error("Payment failed")]
+    PaymentFailed,
+
+    /// Payment pending
+    #[error("Payment pending")]
+    PaymentPending,
+
+    /// Insufficient funds
+    #[error("Insufficient funds")]
+    InsufficientFunds,
+
+    /// Database error
+    #[error("Database error: {msg}")]
+    Database { msg: String },
+
+    /// Network error
+    #[error("Network error: {msg}")]
+    Network { msg: String },
+
+    /// Invalid token
+    #[error("Invalid token: {msg}")]
+    InvalidToken { msg: String },
+
+    /// Wallet error
+    #[error("Wallet error: {msg}")]
+    Wallet { msg: String },
+
+    /// Keyset unknown
+    #[error("Keyset unknown")]
+    KeysetUnknown,
+
+    /// Unit not supported
+    #[error("Unit not supported")]
+    UnitNotSupported,
+
+    /// Runtime task join error
+    #[error("Runtime task join error: {msg}")]
+    RuntimeTaskJoin { msg: String },
+
+    /// Invalid mnemonic phrase
+    #[error("Invalid mnemonic: {msg}")]
+    InvalidMnemonic { msg: String },
+
+    /// URL parsing error
+    #[error("Invalid URL: {msg}")]
+    InvalidUrl { msg: String },
+
+    /// Hex format error
+    #[error("Invalid hex format: {msg}")]
+    InvalidHex { msg: String },
+
+    /// Cryptographic key parsing error
+    #[error("Invalid cryptographic key: {msg}")]
+    InvalidCryptographicKey { msg: String },
+
+    /// Serialization/deserialization error
+    #[error("Serialization error: {msg}")]
+    Serialization { msg: String },
+}
+
+impl From<CdkError> for FfiError {
+    fn from(err: CdkError) -> Self {
+        match err {
+            CdkError::AmountOverflow => FfiError::AmountOverflow,
+            CdkError::PaymentFailed => FfiError::PaymentFailed,
+            CdkError::PaymentPending => FfiError::PaymentPending,
+            CdkError::InsufficientFunds => FfiError::InsufficientFunds,
+            CdkError::UnsupportedUnit => FfiError::UnitNotSupported,
+            CdkError::KeysetUnknown(_) => FfiError::KeysetUnknown,
+            _ => FfiError::Generic {
+                msg: err.to_string(),
+            },
+        }
+    }
+}
+
+impl From<cdk::amount::Error> for FfiError {
+    fn from(err: cdk::amount::Error) -> Self {
+        FfiError::Amount {
+            msg: err.to_string(),
+        }
+    }
+}
+
+impl From<cdk_common::nut00::Error> for FfiError {
+    fn from(err: cdk_common::nut00::Error) -> Self {
+        FfiError::Generic {
+            msg: err.to_string(),
+        }
+    }
+}
+
+impl From<serde_json::Error> for FfiError {
+    fn from(err: serde_json::Error) -> Self {
+        FfiError::Serialization {
+            msg: err.to_string(),
+        }
+    }
+}

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

@@ -0,0 +1,285 @@
+//! CDK FFI Bindings
+//!
+//! UniFFI bindings for the CDK Wallet and related types.
+
+pub mod database;
+pub mod error;
+pub mod types;
+pub mod wallet;
+
+pub use database::*;
+pub use error::*;
+pub use types::*;
+pub use wallet::*;
+
+uniffi::setup_scaffolding!();
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_amount_conversion() {
+        let amount = Amount::new(1000);
+        assert_eq!(amount.value, 1000);
+        assert!(!amount.is_zero());
+
+        let zero = Amount::zero();
+        assert!(zero.is_zero());
+    }
+
+    #[test]
+    fn test_currency_unit_conversion() {
+        use cdk::nuts::CurrencyUnit as CdkCurrencyUnit;
+
+        let unit = CurrencyUnit::Sat;
+        let cdk_unit: CdkCurrencyUnit = unit.into();
+        let back: CurrencyUnit = cdk_unit.into();
+        assert_eq!(back, CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_mint_url_creation() {
+        let url = MintUrl::new("https://mint.example.com".to_string());
+        assert!(url.is_ok());
+
+        let invalid_url = MintUrl::new("not-a-url".to_string());
+        assert!(invalid_url.is_err());
+    }
+
+    #[test]
+    fn test_send_options_default() {
+        let options = SendOptions::default();
+        assert!(options.memo.is_none());
+        assert!(options.conditions.is_none());
+        assert!(matches!(options.amount_split_target, SplitTarget::None));
+        assert!(matches!(options.send_kind, SendKind::OnlineExact));
+        assert!(!options.include_fee);
+        assert!(options.max_proofs.is_none());
+        assert!(options.metadata.is_empty());
+    }
+
+    #[test]
+    fn test_receive_options_default() {
+        let options = ReceiveOptions::default();
+        assert!(matches!(options.amount_split_target, SplitTarget::None));
+        assert!(options.p2pk_signing_keys.is_empty());
+        assert!(options.preimages.is_empty());
+        assert!(options.metadata.is_empty());
+    }
+
+    #[test]
+    fn test_send_memo() {
+        let memo_text = "Test memo".to_string();
+        let memo = SendMemo {
+            memo: memo_text.clone(),
+            include_memo: true,
+        };
+
+        assert_eq!(memo.memo, memo_text);
+        assert!(memo.include_memo);
+    }
+
+    #[test]
+    fn test_split_target_variants() {
+        let split_none = SplitTarget::None;
+        assert!(matches!(split_none, SplitTarget::None));
+
+        let amount = Amount::new(1000);
+        let split_value = SplitTarget::Value { amount };
+        assert!(matches!(split_value, SplitTarget::Value { .. }));
+
+        let amounts = vec![Amount::new(100), Amount::new(200)];
+        let split_values = SplitTarget::Values { amounts };
+        assert!(matches!(split_values, SplitTarget::Values { .. }));
+    }
+
+    #[test]
+    fn test_send_kind_variants() {
+        let online_exact = SendKind::OnlineExact;
+        assert!(matches!(online_exact, SendKind::OnlineExact));
+
+        let tolerance = Amount::new(50);
+        let online_tolerance = SendKind::OnlineTolerance { tolerance };
+        assert!(matches!(online_tolerance, SendKind::OnlineTolerance { .. }));
+
+        let offline_exact = SendKind::OfflineExact;
+        assert!(matches!(offline_exact, SendKind::OfflineExact));
+
+        let offline_tolerance = SendKind::OfflineTolerance { tolerance };
+        assert!(matches!(
+            offline_tolerance,
+            SendKind::OfflineTolerance { .. }
+        ));
+    }
+
+    #[test]
+    fn test_secret_key_from_hex() {
+        // Test valid hex string (64 characters)
+        let valid_hex = "a".repeat(64);
+        let secret_key = SecretKey::from_hex(valid_hex.clone());
+        assert!(secret_key.is_ok());
+        assert_eq!(secret_key.unwrap().hex, valid_hex);
+
+        // Test invalid length
+        let invalid_length = "a".repeat(32); // 32 chars instead of 64
+        let secret_key = SecretKey::from_hex(invalid_length);
+        assert!(secret_key.is_err());
+
+        // Test invalid characters
+        let invalid_chars = "g".repeat(64); // 'g' is not a valid hex character
+        let secret_key = SecretKey::from_hex(invalid_chars);
+        assert!(secret_key.is_err());
+    }
+
+    #[test]
+    fn test_secret_key_random() {
+        let key1 = SecretKey::random();
+        let key2 = SecretKey::random();
+
+        // Keys should be different
+        assert_ne!(key1.hex, key2.hex);
+
+        // Keys should be valid hex (64 characters)
+        assert_eq!(key1.hex.len(), 64);
+        assert_eq!(key2.hex.len(), 64);
+        assert!(key1.hex.chars().all(|c| c.is_ascii_hexdigit()));
+        assert!(key2.hex.chars().all(|c| c.is_ascii_hexdigit()));
+    }
+
+    #[test]
+    fn test_send_options_with_all_fields() {
+        use std::collections::HashMap;
+
+        let memo = SendMemo {
+            memo: "Test memo".to_string(),
+            include_memo: true,
+        };
+
+        let mut metadata = HashMap::new();
+        metadata.insert("key1".to_string(), "value1".to_string());
+
+        let conditions = SpendingConditions::P2PK {
+            pubkey: "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"
+                .to_string(),
+            conditions: None,
+        };
+
+        let options = SendOptions {
+            memo: Some(memo),
+            conditions: Some(conditions),
+            amount_split_target: SplitTarget::Value {
+                amount: Amount::new(1000),
+            },
+            send_kind: SendKind::OnlineTolerance {
+                tolerance: Amount::new(50),
+            },
+            include_fee: true,
+            max_proofs: Some(10),
+            metadata,
+        };
+
+        assert!(options.memo.is_some());
+        assert!(options.conditions.is_some());
+        assert!(matches!(
+            options.amount_split_target,
+            SplitTarget::Value { .. }
+        ));
+        assert!(matches!(
+            options.send_kind,
+            SendKind::OnlineTolerance { .. }
+        ));
+        assert!(options.include_fee);
+        assert_eq!(options.max_proofs, Some(10));
+        assert!(!options.metadata.is_empty());
+    }
+
+    #[test]
+    fn test_receive_options_with_all_fields() {
+        use std::collections::HashMap;
+
+        let secret_key = SecretKey::random();
+        let mut metadata = HashMap::new();
+        metadata.insert("key1".to_string(), "value1".to_string());
+
+        let options = ReceiveOptions {
+            amount_split_target: SplitTarget::Values {
+                amounts: vec![Amount::new(100), Amount::new(200)],
+            },
+            p2pk_signing_keys: vec![secret_key],
+            preimages: vec!["preimage1".to_string(), "preimage2".to_string()],
+            metadata,
+        };
+
+        assert!(matches!(
+            options.amount_split_target,
+            SplitTarget::Values { .. }
+        ));
+        assert_eq!(options.p2pk_signing_keys.len(), 1);
+        assert_eq!(options.preimages.len(), 2);
+        assert!(!options.metadata.is_empty());
+    }
+
+    #[test]
+    fn test_wallet_config() {
+        let config = WalletConfig {
+            target_proof_count: None,
+        };
+        assert!(config.target_proof_count.is_none());
+
+        let config_with_values = WalletConfig {
+            target_proof_count: Some(5),
+        };
+        assert_eq!(config_with_values.target_proof_count, Some(5));
+    }
+
+    #[test]
+    fn test_mnemonic_generation() {
+        // Test mnemonic generation
+        let mnemonic = generate_mnemonic().unwrap();
+        assert!(!mnemonic.is_empty());
+        assert_eq!(mnemonic.split_whitespace().count(), 12);
+
+        // Verify it's a valid mnemonic by trying to parse it
+        use bip39::Mnemonic;
+        let parsed = Mnemonic::parse(&mnemonic);
+        assert!(parsed.is_ok());
+    }
+
+    #[test]
+    fn test_mnemonic_validation() {
+        // Test with valid mnemonic
+        let mnemonic = generate_mnemonic().unwrap();
+        use bip39::Mnemonic;
+        let parsed = Mnemonic::parse(&mnemonic);
+        assert!(parsed.is_ok());
+
+        // Test with invalid mnemonic
+        let invalid_mnemonic = "invalid mnemonic phrase that should not work";
+        let parsed_invalid = Mnemonic::parse(invalid_mnemonic);
+        assert!(parsed_invalid.is_err());
+
+        // Test mnemonic word count variations
+        let mnemonic_12 = generate_mnemonic().unwrap();
+        assert_eq!(mnemonic_12.split_whitespace().count(), 12);
+    }
+
+    #[test]
+    fn test_mnemonic_to_entropy() {
+        // Test with generated mnemonic
+        let mnemonic = generate_mnemonic().unwrap();
+        let entropy = mnemonic_to_entropy(mnemonic.clone()).unwrap();
+
+        // For a 12-word mnemonic, entropy should be 16 bytes (128 bits)
+        assert_eq!(entropy.len(), 16);
+
+        // Test that we can recreate the mnemonic from entropy
+        use bip39::Mnemonic;
+        let recreated_mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
+        assert_eq!(recreated_mnemonic.to_string(), mnemonic);
+
+        // Test with invalid mnemonic
+        let invalid_result = mnemonic_to_entropy("invalid mnemonic".to_string());
+        assert!(invalid_result.is_err());
+    }
+}

+ 2636 - 0
crates/cdk-ffi/src/types.rs

@@ -0,0 +1,2636 @@
+//! FFI-compatible types
+
+use std::collections::HashMap;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use cdk::nuts::{CurrencyUnit as CdkCurrencyUnit, State as CdkState};
+use cdk::Amount as CdkAmount;
+use cdk_common::pub_sub::SubId;
+use serde::{Deserialize, Serialize};
+
+use crate::error::FfiError;
+
+/// FFI-compatible Amount type
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct Amount {
+    pub value: u64,
+}
+
+impl Amount {
+    pub fn new(value: u64) -> Self {
+        Self { value }
+    }
+
+    pub fn zero() -> Self {
+        Self { value: 0 }
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.value == 0
+    }
+
+    pub fn convert_unit(
+        &self,
+        current_unit: CurrencyUnit,
+        target_unit: CurrencyUnit,
+    ) -> Result<Amount, FfiError> {
+        Ok(CdkAmount::from(self.value)
+            .convert_unit(&current_unit.into(), &target_unit.into())
+            .map(Into::into)?)
+    }
+
+    pub fn add(&self, other: Amount) -> Result<Amount, FfiError> {
+        let self_amount = CdkAmount::from(self.value);
+        let other_amount = CdkAmount::from(other.value);
+        self_amount
+            .checked_add(other_amount)
+            .map(Into::into)
+            .ok_or(FfiError::AmountOverflow)
+    }
+
+    pub fn subtract(&self, other: Amount) -> Result<Amount, FfiError> {
+        let self_amount = CdkAmount::from(self.value);
+        let other_amount = CdkAmount::from(other.value);
+        self_amount
+            .checked_sub(other_amount)
+            .map(Into::into)
+            .ok_or(FfiError::AmountOverflow)
+    }
+
+    pub fn multiply(&self, factor: u64) -> Result<Amount, FfiError> {
+        let self_amount = CdkAmount::from(self.value);
+        let factor_amount = CdkAmount::from(factor);
+        self_amount
+            .checked_mul(factor_amount)
+            .map(Into::into)
+            .ok_or(FfiError::AmountOverflow)
+    }
+
+    pub fn divide(&self, divisor: u64) -> Result<Amount, FfiError> {
+        if divisor == 0 {
+            return Err(FfiError::DivisionByZero);
+        }
+        let self_amount = CdkAmount::from(self.value);
+        let divisor_amount = CdkAmount::from(divisor);
+        self_amount
+            .checked_div(divisor_amount)
+            .map(Into::into)
+            .ok_or(FfiError::AmountOverflow)
+    }
+}
+
+impl From<CdkAmount> for Amount {
+    fn from(amount: CdkAmount) -> Self {
+        Self {
+            value: u64::from(amount),
+        }
+    }
+}
+
+impl From<Amount> for CdkAmount {
+    fn from(amount: Amount) -> Self {
+        CdkAmount::from(amount.value)
+    }
+}
+
+/// FFI-compatible Currency Unit
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum CurrencyUnit {
+    Sat,
+    Msat,
+    Usd,
+    Eur,
+    Auth,
+    Custom { unit: String },
+}
+
+impl From<CdkCurrencyUnit> for CurrencyUnit {
+    fn from(unit: CdkCurrencyUnit) -> Self {
+        match unit {
+            CdkCurrencyUnit::Sat => CurrencyUnit::Sat,
+            CdkCurrencyUnit::Msat => CurrencyUnit::Msat,
+            CdkCurrencyUnit::Usd => CurrencyUnit::Usd,
+            CdkCurrencyUnit::Eur => CurrencyUnit::Eur,
+            CdkCurrencyUnit::Auth => CurrencyUnit::Auth,
+            CdkCurrencyUnit::Custom(s) => CurrencyUnit::Custom { unit: s },
+            _ => CurrencyUnit::Sat, // Default for unknown units
+        }
+    }
+}
+
+impl From<CurrencyUnit> for CdkCurrencyUnit {
+    fn from(unit: CurrencyUnit) -> Self {
+        match unit {
+            CurrencyUnit::Sat => CdkCurrencyUnit::Sat,
+            CurrencyUnit::Msat => CdkCurrencyUnit::Msat,
+            CurrencyUnit::Usd => CdkCurrencyUnit::Usd,
+            CurrencyUnit::Eur => CdkCurrencyUnit::Eur,
+            CurrencyUnit::Auth => CdkCurrencyUnit::Auth,
+            CurrencyUnit::Custom { unit } => CdkCurrencyUnit::Custom(unit),
+        }
+    }
+}
+
+/// FFI-compatible Mint URL
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct MintUrl {
+    pub url: String,
+}
+
+impl MintUrl {
+    pub fn new(url: String) -> Result<Self, FfiError> {
+        // Validate URL format
+        url::Url::parse(&url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;
+
+        Ok(Self { url })
+    }
+}
+
+impl From<cdk::mint_url::MintUrl> for MintUrl {
+    fn from(mint_url: cdk::mint_url::MintUrl) -> Self {
+        Self {
+            url: mint_url.to_string(),
+        }
+    }
+}
+
+impl TryFrom<MintUrl> for cdk::mint_url::MintUrl {
+    type Error = FfiError;
+
+    fn try_from(mint_url: MintUrl) -> Result<Self, Self::Error> {
+        cdk::mint_url::MintUrl::from_str(&mint_url.url)
+            .map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })
+    }
+}
+
+/// FFI-compatible Proof state
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum ProofState {
+    Unspent,
+    Pending,
+    Spent,
+    Reserved,
+    PendingSpent,
+}
+
+impl From<CdkState> for ProofState {
+    fn from(state: CdkState) -> Self {
+        match state {
+            CdkState::Unspent => ProofState::Unspent,
+            CdkState::Pending => ProofState::Pending,
+            CdkState::Spent => ProofState::Spent,
+            CdkState::Reserved => ProofState::Reserved,
+            CdkState::PendingSpent => ProofState::PendingSpent,
+        }
+    }
+}
+
+impl From<ProofState> for CdkState {
+    fn from(state: ProofState) -> Self {
+        match state {
+            ProofState::Unspent => CdkState::Unspent,
+            ProofState::Pending => CdkState::Pending,
+            ProofState::Spent => CdkState::Spent,
+            ProofState::Reserved => CdkState::Reserved,
+            ProofState::PendingSpent => CdkState::PendingSpent,
+        }
+    }
+}
+
+/// FFI-compatible Token
+#[derive(Debug, uniffi::Object)]
+pub struct Token {
+    pub(crate) inner: cdk::nuts::Token,
+}
+
+impl std::fmt::Display for Token {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.inner)
+    }
+}
+
+impl FromStr for Token {
+    type Err = FfiError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let token = cdk::nuts::Token::from_str(s)
+            .map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
+        Ok(Token { inner: token })
+    }
+}
+
+impl From<cdk::nuts::Token> for Token {
+    fn from(token: cdk::nuts::Token) -> Self {
+        Self { inner: token }
+    }
+}
+
+impl From<Token> for cdk::nuts::Token {
+    fn from(token: Token) -> Self {
+        token.inner
+    }
+}
+
+#[uniffi::export]
+impl Token {
+    /// Create a new Token from string
+    #[uniffi::constructor]
+    pub fn from_string(encoded_token: String) -> Result<Token, FfiError> {
+        let token = cdk::nuts::Token::from_str(&encoded_token)
+            .map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
+        Ok(Token { inner: token })
+    }
+
+    /// Get the total value of the token
+    pub fn value(&self) -> Result<Amount, FfiError> {
+        Ok(self.inner.value()?.into())
+    }
+
+    /// Get the memo from the token
+    pub fn memo(&self) -> Option<String> {
+        self.inner.memo().clone()
+    }
+
+    /// Get the currency unit
+    pub fn unit(&self) -> Option<CurrencyUnit> {
+        self.inner.unit().map(Into::into)
+    }
+
+    /// Get the mint URL
+    pub fn mint_url(&self) -> Result<MintUrl, FfiError> {
+        Ok(self.inner.mint_url()?.into())
+    }
+
+    /// Get proofs from the token (simplified - no keyset filtering for now)
+    pub fn proofs_simple(&self) -> Result<Proofs, FfiError> {
+        // For now, return empty keysets to get all proofs
+        let empty_keysets = vec![];
+        let proofs = self.inner.proofs(&empty_keysets)?;
+        Ok(proofs
+            .into_iter()
+            .map(|p| std::sync::Arc::new(p.into()))
+            .collect())
+    }
+
+    /// Convert token to raw bytes
+    pub fn to_raw_bytes(&self) -> Result<Vec<u8>, FfiError> {
+        Ok(self.inner.to_raw_bytes()?)
+    }
+
+    /// Encode token to string representation
+    pub fn encode(&self) -> String {
+        self.to_string()
+    }
+
+    /// Decode token from string representation
+    #[uniffi::constructor]
+    pub fn decode(encoded_token: String) -> Result<Token, FfiError> {
+        encoded_token.parse()
+    }
+}
+
+/// FFI-compatible SendMemo
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct SendMemo {
+    /// Memo text
+    pub memo: String,
+    /// Include memo in token
+    pub include_memo: bool,
+}
+
+impl From<SendMemo> for cdk::wallet::SendMemo {
+    fn from(memo: SendMemo) -> Self {
+        cdk::wallet::SendMemo {
+            memo: memo.memo,
+            include_memo: memo.include_memo,
+        }
+    }
+}
+
+impl From<cdk::wallet::SendMemo> for SendMemo {
+    fn from(memo: cdk::wallet::SendMemo) -> Self {
+        Self {
+            memo: memo.memo,
+            include_memo: memo.include_memo,
+        }
+    }
+}
+
+impl SendMemo {
+    /// Convert SendMemo to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode SendMemo from JSON string
+#[uniffi::export]
+pub fn decode_send_memo(json: String) -> Result<SendMemo, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode SendMemo to JSON string
+#[uniffi::export]
+pub fn encode_send_memo(memo: SendMemo) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&memo)?)
+}
+
+/// FFI-compatible SplitTarget
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+pub enum SplitTarget {
+    /// Default target; least amount of proofs
+    None,
+    /// Target amount for wallet to have most proofs that add up to value
+    Value { amount: Amount },
+    /// Specific amounts to split into (must equal amount being split)
+    Values { amounts: Vec<Amount> },
+}
+
+impl From<SplitTarget> for cdk::amount::SplitTarget {
+    fn from(target: SplitTarget) -> Self {
+        match target {
+            SplitTarget::None => cdk::amount::SplitTarget::None,
+            SplitTarget::Value { amount } => cdk::amount::SplitTarget::Value(amount.into()),
+            SplitTarget::Values { amounts } => {
+                cdk::amount::SplitTarget::Values(amounts.into_iter().map(Into::into).collect())
+            }
+        }
+    }
+}
+
+impl From<cdk::amount::SplitTarget> for SplitTarget {
+    fn from(target: cdk::amount::SplitTarget) -> Self {
+        match target {
+            cdk::amount::SplitTarget::None => SplitTarget::None,
+            cdk::amount::SplitTarget::Value(amount) => SplitTarget::Value {
+                amount: amount.into(),
+            },
+            cdk::amount::SplitTarget::Values(amounts) => SplitTarget::Values {
+                amounts: amounts.into_iter().map(Into::into).collect(),
+            },
+        }
+    }
+}
+
+/// FFI-compatible SendKind
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+pub enum SendKind {
+    /// Allow online swap before send if wallet does not have exact amount
+    OnlineExact,
+    /// Prefer offline send if difference is less than tolerance
+    OnlineTolerance { tolerance: Amount },
+    /// Wallet cannot do an online swap and selected proof must be exactly send amount
+    OfflineExact,
+    /// Wallet must remain offline but can over pay if below tolerance
+    OfflineTolerance { tolerance: Amount },
+}
+
+impl From<SendKind> for cdk_common::wallet::SendKind {
+    fn from(kind: SendKind) -> Self {
+        match kind {
+            SendKind::OnlineExact => cdk_common::wallet::SendKind::OnlineExact,
+            SendKind::OnlineTolerance { tolerance } => {
+                cdk_common::wallet::SendKind::OnlineTolerance(tolerance.into())
+            }
+            SendKind::OfflineExact => cdk_common::wallet::SendKind::OfflineExact,
+            SendKind::OfflineTolerance { tolerance } => {
+                cdk_common::wallet::SendKind::OfflineTolerance(tolerance.into())
+            }
+        }
+    }
+}
+
+impl From<cdk_common::wallet::SendKind> for SendKind {
+    fn from(kind: cdk_common::wallet::SendKind) -> Self {
+        match kind {
+            cdk_common::wallet::SendKind::OnlineExact => SendKind::OnlineExact,
+            cdk_common::wallet::SendKind::OnlineTolerance(tolerance) => SendKind::OnlineTolerance {
+                tolerance: tolerance.into(),
+            },
+            cdk_common::wallet::SendKind::OfflineExact => SendKind::OfflineExact,
+            cdk_common::wallet::SendKind::OfflineTolerance(tolerance) => {
+                SendKind::OfflineTolerance {
+                    tolerance: tolerance.into(),
+                }
+            }
+        }
+    }
+}
+
+/// FFI-compatible Send options
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct SendOptions {
+    /// Memo
+    pub memo: Option<SendMemo>,
+    /// Spending conditions
+    pub conditions: Option<SpendingConditions>,
+    /// Amount split target
+    pub amount_split_target: SplitTarget,
+    /// Send kind
+    pub send_kind: SendKind,
+    /// Include fee
+    pub include_fee: bool,
+    /// Maximum number of proofs to include in the token
+    pub max_proofs: Option<u32>,
+    /// Metadata
+    pub metadata: HashMap<String, String>,
+}
+
+impl Default for SendOptions {
+    fn default() -> Self {
+        Self {
+            memo: None,
+            conditions: None,
+            amount_split_target: SplitTarget::None,
+            send_kind: SendKind::OnlineExact,
+            include_fee: false,
+            max_proofs: None,
+            metadata: HashMap::new(),
+        }
+    }
+}
+
+impl From<SendOptions> for cdk::wallet::SendOptions {
+    fn from(opts: SendOptions) -> Self {
+        cdk::wallet::SendOptions {
+            memo: opts.memo.map(Into::into),
+            conditions: opts.conditions.and_then(|c| c.try_into().ok()),
+            amount_split_target: opts.amount_split_target.into(),
+            send_kind: opts.send_kind.into(),
+            include_fee: opts.include_fee,
+            max_proofs: opts.max_proofs.map(|p| p as usize),
+            metadata: opts.metadata,
+        }
+    }
+}
+
+impl From<cdk::wallet::SendOptions> for SendOptions {
+    fn from(opts: cdk::wallet::SendOptions) -> Self {
+        Self {
+            memo: opts.memo.map(Into::into),
+            conditions: opts.conditions.map(Into::into),
+            amount_split_target: opts.amount_split_target.into(),
+            send_kind: opts.send_kind.into(),
+            include_fee: opts.include_fee,
+            max_proofs: opts.max_proofs.map(|p| p as u32),
+            metadata: opts.metadata,
+        }
+    }
+}
+
+impl SendOptions {
+    /// Convert SendOptions to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode SendOptions from JSON string
+#[uniffi::export]
+pub fn decode_send_options(json: String) -> Result<SendOptions, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode SendOptions to JSON string
+#[uniffi::export]
+pub fn encode_send_options(options: SendOptions) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&options)?)
+}
+
+/// FFI-compatible SecretKey
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct SecretKey {
+    /// Hex-encoded secret key (64 characters)
+    pub hex: String,
+}
+
+impl SecretKey {
+    /// Create a new SecretKey from hex string
+    pub fn from_hex(hex: String) -> Result<Self, FfiError> {
+        // Validate hex string length (should be 64 characters for 32 bytes)
+        if hex.len() != 64 {
+            return Err(FfiError::InvalidHex {
+                msg: "Secret key hex must be exactly 64 characters (32 bytes)".to_string(),
+            });
+        }
+
+        // Validate hex format
+        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
+            return Err(FfiError::InvalidHex {
+                msg: "Secret key hex contains invalid characters".to_string(),
+            });
+        }
+
+        Ok(Self { hex })
+    }
+
+    /// Generate a random secret key
+    pub fn random() -> Self {
+        use cdk::nuts::SecretKey as CdkSecretKey;
+        let secret_key = CdkSecretKey::generate();
+        Self {
+            hex: secret_key.to_secret_hex(),
+        }
+    }
+}
+
+impl From<SecretKey> for cdk::nuts::SecretKey {
+    fn from(key: SecretKey) -> Self {
+        // This will panic if hex is invalid, but we validate in from_hex()
+        cdk::nuts::SecretKey::from_hex(&key.hex).expect("Invalid secret key hex")
+    }
+}
+
+impl From<cdk::nuts::SecretKey> for SecretKey {
+    fn from(key: cdk::nuts::SecretKey) -> Self {
+        Self {
+            hex: key.to_secret_hex(),
+        }
+    }
+}
+
+/// FFI-compatible Receive options
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ReceiveOptions {
+    /// Amount split target
+    pub amount_split_target: SplitTarget,
+    /// P2PK signing keys
+    pub p2pk_signing_keys: Vec<SecretKey>,
+    /// Preimages for HTLC conditions
+    pub preimages: Vec<String>,
+    /// Metadata
+    pub metadata: HashMap<String, String>,
+}
+
+impl Default for ReceiveOptions {
+    fn default() -> Self {
+        Self {
+            amount_split_target: SplitTarget::None,
+            p2pk_signing_keys: Vec::new(),
+            preimages: Vec::new(),
+            metadata: HashMap::new(),
+        }
+    }
+}
+
+impl From<ReceiveOptions> for cdk::wallet::ReceiveOptions {
+    fn from(opts: ReceiveOptions) -> Self {
+        cdk::wallet::ReceiveOptions {
+            amount_split_target: opts.amount_split_target.into(),
+            p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(),
+            preimages: opts.preimages,
+            metadata: opts.metadata,
+        }
+    }
+}
+
+impl From<cdk::wallet::ReceiveOptions> for ReceiveOptions {
+    fn from(opts: cdk::wallet::ReceiveOptions) -> Self {
+        Self {
+            amount_split_target: opts.amount_split_target.into(),
+            p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(),
+            preimages: opts.preimages,
+            metadata: opts.metadata,
+        }
+    }
+}
+
+impl ReceiveOptions {
+    /// Convert ReceiveOptions to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ReceiveOptions from JSON string
+#[uniffi::export]
+pub fn decode_receive_options(json: String) -> Result<ReceiveOptions, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ReceiveOptions to JSON string
+#[uniffi::export]
+pub fn encode_receive_options(options: ReceiveOptions) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&options)?)
+}
+
+/// FFI-compatible Proof
+#[derive(Debug, uniffi::Object)]
+pub struct Proof {
+    pub(crate) inner: cdk::nuts::Proof,
+}
+
+impl From<cdk::nuts::Proof> for Proof {
+    fn from(proof: cdk::nuts::Proof) -> Self {
+        Self { inner: proof }
+    }
+}
+
+impl From<Proof> for cdk::nuts::Proof {
+    fn from(proof: Proof) -> Self {
+        proof.inner
+    }
+}
+
+#[uniffi::export]
+impl Proof {
+    /// Get the amount
+    pub fn amount(&self) -> Amount {
+        self.inner.amount.into()
+    }
+
+    /// Get the secret as string
+    pub fn secret(&self) -> String {
+        self.inner.secret.to_string()
+    }
+
+    /// Get the unblinded signature (C) as string
+    pub fn c(&self) -> String {
+        self.inner.c.to_string()
+    }
+
+    /// Get the keyset ID as string
+    pub fn keyset_id(&self) -> String {
+        self.inner.keyset_id.to_string()
+    }
+
+    /// Get the witness
+    pub fn witness(&self) -> Option<Witness> {
+        self.inner.witness.as_ref().map(|w| w.clone().into())
+    }
+
+    /// Check if proof is active with given keyset IDs
+    pub fn is_active(&self, active_keyset_ids: Vec<String>) -> bool {
+        use cdk::nuts::Id;
+        let ids: Vec<Id> = active_keyset_ids
+            .into_iter()
+            .filter_map(|id| Id::from_str(&id).ok())
+            .collect();
+        self.inner.is_active(&ids)
+    }
+
+    /// Get the Y value (hash_to_curve of secret)
+    pub fn y(&self) -> Result<String, FfiError> {
+        Ok(self.inner.y()?.to_string())
+    }
+
+    /// Get the DLEQ proof if present
+    pub fn dleq(&self) -> Option<ProofDleq> {
+        self.inner.dleq.as_ref().map(|d| d.clone().into())
+    }
+
+    /// Check if proof has DLEQ proof
+    pub fn has_dleq(&self) -> bool {
+        self.inner.dleq.is_some()
+    }
+}
+
+/// FFI-compatible Proofs (vector of Proof)
+pub type Proofs = Vec<std::sync::Arc<Proof>>;
+
+/// FFI-compatible DLEQ proof for proofs
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ProofDleq {
+    /// e value (hex-encoded SecretKey)
+    pub e: String,
+    /// s value (hex-encoded SecretKey)
+    pub s: String,
+    /// r value - blinding factor (hex-encoded SecretKey)
+    pub r: String,
+}
+
+/// FFI-compatible DLEQ proof for blind signatures
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct BlindSignatureDleq {
+    /// e value (hex-encoded SecretKey)
+    pub e: String,
+    /// s value (hex-encoded SecretKey)
+    pub s: String,
+}
+
+impl From<cdk::nuts::ProofDleq> for ProofDleq {
+    fn from(dleq: cdk::nuts::ProofDleq) -> Self {
+        Self {
+            e: dleq.e.to_secret_hex(),
+            s: dleq.s.to_secret_hex(),
+            r: dleq.r.to_secret_hex(),
+        }
+    }
+}
+
+impl From<ProofDleq> for cdk::nuts::ProofDleq {
+    fn from(dleq: ProofDleq) -> Self {
+        Self {
+            e: cdk::nuts::SecretKey::from_hex(&dleq.e).expect("Invalid e hex"),
+            s: cdk::nuts::SecretKey::from_hex(&dleq.s).expect("Invalid s hex"),
+            r: cdk::nuts::SecretKey::from_hex(&dleq.r).expect("Invalid r hex"),
+        }
+    }
+}
+
+impl From<cdk::nuts::BlindSignatureDleq> for BlindSignatureDleq {
+    fn from(dleq: cdk::nuts::BlindSignatureDleq) -> Self {
+        Self {
+            e: dleq.e.to_secret_hex(),
+            s: dleq.s.to_secret_hex(),
+        }
+    }
+}
+
+impl From<BlindSignatureDleq> for cdk::nuts::BlindSignatureDleq {
+    fn from(dleq: BlindSignatureDleq) -> Self {
+        Self {
+            e: cdk::nuts::SecretKey::from_hex(&dleq.e).expect("Invalid e hex"),
+            s: cdk::nuts::SecretKey::from_hex(&dleq.s).expect("Invalid s hex"),
+        }
+    }
+}
+
+/// Helper functions for Proofs
+pub fn proofs_total_amount(proofs: &Proofs) -> Result<Amount, FfiError> {
+    let cdk_proofs: Vec<cdk::nuts::Proof> = proofs.iter().map(|p| p.inner.clone()).collect();
+    use cdk::nuts::ProofsMethods;
+    Ok(cdk_proofs.total_amount()?.into())
+}
+
+/// FFI-compatible MintQuote
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MintQuote {
+    /// Quote ID
+    pub id: String,
+    /// Quote amount
+    pub amount: Option<Amount>,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Payment request
+    pub request: String,
+    /// Quote state
+    pub state: QuoteState,
+    /// Expiry timestamp
+    pub expiry: u64,
+    /// Mint URL
+    pub mint_url: MintUrl,
+    /// Amount issued
+    pub amount_issued: Amount,
+    /// Amount paid
+    pub amount_paid: Amount,
+    /// Payment method
+    pub payment_method: PaymentMethod,
+    /// Secret key (optional, hex-encoded)
+    pub secret_key: Option<String>,
+}
+
+impl From<cdk::wallet::MintQuote> for MintQuote {
+    fn from(quote: cdk::wallet::MintQuote) -> Self {
+        Self {
+            id: quote.id.clone(),
+            amount: quote.amount.map(Into::into),
+            unit: quote.unit.clone().into(),
+            request: quote.request.clone(),
+            state: quote.state.into(),
+            expiry: quote.expiry,
+            mint_url: quote.mint_url.clone().into(),
+            amount_issued: quote.amount_issued.into(),
+            amount_paid: quote.amount_paid.into(),
+            payment_method: quote.payment_method.into(),
+            secret_key: quote.secret_key.map(|sk| sk.to_secret_hex()),
+        }
+    }
+}
+
+impl TryFrom<MintQuote> for cdk::wallet::MintQuote {
+    type Error = FfiError;
+
+    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
+        let secret_key = quote
+            .secret_key
+            .map(|hex| cdk::nuts::SecretKey::from_hex(&hex))
+            .transpose()
+            .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
+
+        Ok(Self {
+            id: quote.id,
+            amount: quote.amount.map(Into::into),
+            unit: quote.unit.into(),
+            request: quote.request,
+            state: quote.state.into(),
+            expiry: quote.expiry,
+            mint_url: quote.mint_url.try_into()?,
+            amount_issued: quote.amount_issued.into(),
+            amount_paid: quote.amount_paid.into(),
+            payment_method: quote.payment_method.into(),
+            secret_key,
+        })
+    }
+}
+
+impl MintQuote {
+    /// Get total amount (amount + fees)
+    pub fn total_amount(&self) -> Amount {
+        if let Some(amount) = self.amount {
+            Amount::new(amount.value + self.amount_paid.value - self.amount_issued.value)
+        } else {
+            Amount::zero()
+        }
+    }
+
+    /// Check if quote is expired
+    pub fn is_expired(&self, current_time: u64) -> bool {
+        current_time > self.expiry
+    }
+
+    /// Get amount that can be minted
+    pub fn amount_mintable(&self) -> Amount {
+        Amount::new(self.amount_paid.value - self.amount_issued.value)
+    }
+
+    /// Convert MintQuote to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MintQuote from JSON string
+#[uniffi::export]
+pub fn decode_mint_quote(json: String) -> Result<MintQuote, FfiError> {
+    let quote: cdk::wallet::MintQuote = serde_json::from_str(&json)?;
+    Ok(quote.into())
+}
+
+/// Encode MintQuote to JSON string
+#[uniffi::export]
+pub fn encode_mint_quote(quote: MintQuote) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&quote)?)
+}
+
+/// FFI-compatible MintQuoteBolt11Response
+#[derive(Debug, uniffi::Object)]
+pub struct MintQuoteBolt11Response {
+    /// Quote ID
+    pub quote: String,
+    /// Request string
+    pub request: String,
+    /// State of the quote
+    pub state: QuoteState,
+    /// Expiry timestamp (optional)
+    pub expiry: Option<u64>,
+    /// Amount (optional)
+    pub amount: Option<Amount>,
+    /// Unit (optional)
+    pub unit: Option<CurrencyUnit>,
+    /// Pubkey (optional)
+    pub pubkey: Option<String>,
+}
+
+impl From<cdk::nuts::MintQuoteBolt11Response<String>> for MintQuoteBolt11Response {
+    fn from(response: cdk::nuts::MintQuoteBolt11Response<String>) -> Self {
+        Self {
+            quote: response.quote,
+            request: response.request,
+            state: response.state.into(),
+            expiry: response.expiry,
+            amount: response.amount.map(Into::into),
+            unit: response.unit.map(Into::into),
+            pubkey: response.pubkey.map(|p| p.to_string()),
+        }
+    }
+}
+
+#[uniffi::export]
+impl MintQuoteBolt11Response {
+    /// Get quote ID
+    pub fn quote(&self) -> String {
+        self.quote.clone()
+    }
+
+    /// Get request string
+    pub fn request(&self) -> String {
+        self.request.clone()
+    }
+
+    /// Get state
+    pub fn state(&self) -> QuoteState {
+        self.state.clone()
+    }
+
+    /// Get expiry
+    pub fn expiry(&self) -> Option<u64> {
+        self.expiry
+    }
+
+    /// Get amount
+    pub fn amount(&self) -> Option<Amount> {
+        self.amount
+    }
+
+    /// Get unit
+    pub fn unit(&self) -> Option<CurrencyUnit> {
+        self.unit.clone()
+    }
+
+    /// Get pubkey
+    pub fn pubkey(&self) -> Option<String> {
+        self.pubkey.clone()
+    }
+}
+
+/// FFI-compatible MeltQuoteBolt11Response
+#[derive(Debug, uniffi::Object)]
+pub struct MeltQuoteBolt11Response {
+    /// Quote ID
+    pub quote: String,
+    /// Amount
+    pub amount: Amount,
+    /// Fee reserve
+    pub fee_reserve: Amount,
+    /// State of the quote
+    pub state: QuoteState,
+    /// Expiry timestamp
+    pub expiry: u64,
+    /// Payment preimage (optional)
+    pub payment_preimage: Option<String>,
+    /// Request string (optional)
+    pub request: Option<String>,
+    /// Unit (optional)
+    pub unit: Option<CurrencyUnit>,
+}
+
+impl From<cdk::nuts::MeltQuoteBolt11Response<String>> for MeltQuoteBolt11Response {
+    fn from(response: cdk::nuts::MeltQuoteBolt11Response<String>) -> Self {
+        Self {
+            quote: response.quote,
+            amount: response.amount.into(),
+            fee_reserve: response.fee_reserve.into(),
+            state: response.state.into(),
+            expiry: response.expiry,
+            payment_preimage: response.payment_preimage,
+            request: response.request,
+            unit: response.unit.map(Into::into),
+        }
+    }
+}
+
+#[uniffi::export]
+impl MeltQuoteBolt11Response {
+    /// Get quote ID
+    pub fn quote(&self) -> String {
+        self.quote.clone()
+    }
+
+    /// Get amount
+    pub fn amount(&self) -> Amount {
+        self.amount
+    }
+
+    /// Get fee reserve
+    pub fn fee_reserve(&self) -> Amount {
+        self.fee_reserve
+    }
+
+    /// Get state
+    pub fn state(&self) -> QuoteState {
+        self.state.clone()
+    }
+
+    /// Get expiry
+    pub fn expiry(&self) -> u64 {
+        self.expiry
+    }
+
+    /// Get payment preimage
+    pub fn payment_preimage(&self) -> Option<String> {
+        self.payment_preimage.clone()
+    }
+
+    /// Get request
+    pub fn request(&self) -> Option<String> {
+        self.request.clone()
+    }
+
+    /// Get unit
+    pub fn unit(&self) -> Option<CurrencyUnit> {
+        self.unit.clone()
+    }
+}
+
+/// FFI-compatible PaymentMethod
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum PaymentMethod {
+    /// Bolt11 payment type
+    Bolt11,
+    /// Bolt12 payment type
+    Bolt12,
+    /// Custom payment type
+    Custom { method: String },
+}
+
+impl From<cdk::nuts::PaymentMethod> for PaymentMethod {
+    fn from(method: cdk::nuts::PaymentMethod) -> Self {
+        match method {
+            cdk::nuts::PaymentMethod::Bolt11 => Self::Bolt11,
+            cdk::nuts::PaymentMethod::Bolt12 => Self::Bolt12,
+            cdk::nuts::PaymentMethod::Custom(s) => Self::Custom { method: s },
+        }
+    }
+}
+
+impl From<PaymentMethod> for cdk::nuts::PaymentMethod {
+    fn from(method: PaymentMethod) -> Self {
+        match method {
+            PaymentMethod::Bolt11 => Self::Bolt11,
+            PaymentMethod::Bolt12 => Self::Bolt12,
+            PaymentMethod::Custom { method } => Self::Custom(method),
+        }
+    }
+}
+
+/// FFI-compatible MeltQuote
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MeltQuote {
+    /// Quote ID
+    pub id: String,
+    /// Quote amount
+    pub amount: Amount,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Payment request
+    pub request: String,
+    /// Fee reserve
+    pub fee_reserve: Amount,
+    /// Quote state
+    pub state: QuoteState,
+    /// Expiry timestamp
+    pub expiry: u64,
+    /// Payment preimage
+    pub payment_preimage: Option<String>,
+    /// Payment method
+    pub payment_method: PaymentMethod,
+}
+
+impl From<cdk::wallet::MeltQuote> for MeltQuote {
+    fn from(quote: cdk::wallet::MeltQuote) -> Self {
+        Self {
+            id: quote.id.clone(),
+            amount: quote.amount.into(),
+            unit: quote.unit.clone().into(),
+            request: quote.request.clone(),
+            fee_reserve: quote.fee_reserve.into(),
+            state: quote.state.into(),
+            expiry: quote.expiry,
+            payment_preimage: quote.payment_preimage.clone(),
+            payment_method: quote.payment_method.into(),
+        }
+    }
+}
+
+impl TryFrom<MeltQuote> for cdk::wallet::MeltQuote {
+    type Error = FfiError;
+
+    fn try_from(quote: MeltQuote) -> Result<Self, Self::Error> {
+        Ok(Self {
+            id: quote.id,
+            amount: quote.amount.into(),
+            unit: quote.unit.into(),
+            request: quote.request,
+            fee_reserve: quote.fee_reserve.into(),
+            state: quote.state.into(),
+            expiry: quote.expiry,
+            payment_preimage: quote.payment_preimage,
+            payment_method: quote.payment_method.into(),
+        })
+    }
+}
+
+impl MeltQuote {
+    /// Convert MeltQuote to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MeltQuote from JSON string
+#[uniffi::export]
+pub fn decode_melt_quote(json: String) -> Result<MeltQuote, FfiError> {
+    let quote: cdk::wallet::MeltQuote = serde_json::from_str(&json)?;
+    Ok(quote.into())
+}
+
+/// Encode MeltQuote to JSON string
+#[uniffi::export]
+pub fn encode_melt_quote(quote: MeltQuote) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&quote)?)
+}
+
+/// FFI-compatible QuoteState
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum QuoteState {
+    Unpaid,
+    Paid,
+    Pending,
+    Issued,
+}
+
+impl From<cdk::nuts::nut05::QuoteState> for QuoteState {
+    fn from(state: cdk::nuts::nut05::QuoteState) -> Self {
+        match state {
+            cdk::nuts::nut05::QuoteState::Unpaid => QuoteState::Unpaid,
+            cdk::nuts::nut05::QuoteState::Paid => QuoteState::Paid,
+            cdk::nuts::nut05::QuoteState::Pending => QuoteState::Pending,
+            cdk::nuts::nut05::QuoteState::Unknown => QuoteState::Unpaid,
+            cdk::nuts::nut05::QuoteState::Failed => QuoteState::Unpaid,
+        }
+    }
+}
+
+impl From<QuoteState> for cdk::nuts::nut05::QuoteState {
+    fn from(state: QuoteState) -> Self {
+        match state {
+            QuoteState::Unpaid => cdk::nuts::nut05::QuoteState::Unpaid,
+            QuoteState::Paid => cdk::nuts::nut05::QuoteState::Paid,
+            QuoteState::Pending => cdk::nuts::nut05::QuoteState::Pending,
+            QuoteState::Issued => cdk::nuts::nut05::QuoteState::Paid, // Map issued to paid for melt quotes
+        }
+    }
+}
+
+impl From<cdk::nuts::MintQuoteState> for QuoteState {
+    fn from(state: cdk::nuts::MintQuoteState) -> Self {
+        match state {
+            cdk::nuts::MintQuoteState::Unpaid => QuoteState::Unpaid,
+            cdk::nuts::MintQuoteState::Paid => QuoteState::Paid,
+            cdk::nuts::MintQuoteState::Issued => QuoteState::Issued,
+        }
+    }
+}
+
+impl From<QuoteState> for cdk::nuts::MintQuoteState {
+    fn from(state: QuoteState) -> Self {
+        match state {
+            QuoteState::Unpaid => cdk::nuts::MintQuoteState::Unpaid,
+            QuoteState::Paid => cdk::nuts::MintQuoteState::Paid,
+            QuoteState::Issued => cdk::nuts::MintQuoteState::Issued,
+            QuoteState::Pending => cdk::nuts::MintQuoteState::Paid, // Map pending to paid
+        }
+    }
+}
+
+// Note: MeltQuoteState is the same as nut05::QuoteState, so we don't need a separate impl
+
+/// FFI-compatible PreparedSend
+#[derive(Debug, uniffi::Object)]
+pub struct PreparedSend {
+    inner: Mutex<Option<cdk::wallet::PreparedSend>>,
+    id: String,
+    amount: Amount,
+    proofs: Proofs,
+}
+
+impl From<cdk::wallet::PreparedSend> for PreparedSend {
+    fn from(prepared: cdk::wallet::PreparedSend) -> Self {
+        let id = format!("{:?}", prepared); // Use debug format as ID
+        let amount = prepared.amount().into();
+        let proofs = prepared
+            .proofs()
+            .iter()
+            .cloned()
+            .map(|p| std::sync::Arc::new(p.into()))
+            .collect();
+        Self {
+            inner: Mutex::new(Some(prepared)),
+            id,
+            amount,
+            proofs,
+        }
+    }
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+impl PreparedSend {
+    /// Get the prepared send ID
+    pub fn id(&self) -> String {
+        self.id.clone()
+    }
+
+    /// Get the amount to send
+    pub fn amount(&self) -> Amount {
+        self.amount
+    }
+
+    /// Get the proofs that will be used
+    pub fn proofs(&self) -> Proofs {
+        self.proofs.clone()
+    }
+
+    /// Get the total fee for this send operation
+    pub fn fee(&self) -> Amount {
+        if let Ok(guard) = self.inner.lock() {
+            if let Some(ref inner) = *guard {
+                inner.fee().into()
+            } else {
+                Amount::new(0)
+            }
+        } else {
+            Amount::new(0)
+        }
+    }
+
+    /// Confirm the prepared send and create a token
+    pub async fn confirm(
+        self: std::sync::Arc<Self>,
+        memo: Option<String>,
+    ) -> Result<Token, FfiError> {
+        let inner = {
+            if let Ok(mut guard) = self.inner.lock() {
+                guard.take()
+            } else {
+                return Err(FfiError::Generic {
+                    msg: "Failed to acquire lock on PreparedSend".to_string(),
+                });
+            }
+        };
+
+        if let Some(inner) = inner {
+            let send_memo = memo.map(|m| cdk::wallet::SendMemo::for_token(&m));
+            let token = inner.confirm(send_memo).await?;
+            Ok(token.into())
+        } else {
+            Err(FfiError::Generic {
+                msg: "PreparedSend has already been consumed or cancelled".to_string(),
+            })
+        }
+    }
+
+    /// Cancel the prepared send operation
+    pub async fn cancel(self: std::sync::Arc<Self>) -> Result<(), FfiError> {
+        let inner = {
+            if let Ok(mut guard) = self.inner.lock() {
+                guard.take()
+            } else {
+                return Err(FfiError::Generic {
+                    msg: "Failed to acquire lock on PreparedSend".to_string(),
+                });
+            }
+        };
+
+        if let Some(inner) = inner {
+            inner.cancel().await?;
+            Ok(())
+        } else {
+            Err(FfiError::Generic {
+                msg: "PreparedSend has already been consumed or cancelled".to_string(),
+            })
+        }
+    }
+}
+
+/// FFI-compatible Melted result
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct Melted {
+    pub state: QuoteState,
+    pub preimage: Option<String>,
+    pub change: Option<Proofs>,
+    pub amount: Amount,
+    pub fee_paid: Amount,
+}
+
+// MeltQuoteState is just an alias for nut05::QuoteState, so we don't need a separate implementation
+
+impl From<cdk_common::common::Melted> for Melted {
+    fn from(melted: cdk_common::common::Melted) -> Self {
+        Self {
+            state: melted.state.into(),
+            preimage: melted.preimage,
+            change: melted.change.map(|proofs| {
+                proofs
+                    .into_iter()
+                    .map(|p| std::sync::Arc::new(p.into()))
+                    .collect()
+            }),
+            amount: melted.amount.into(),
+            fee_paid: melted.fee_paid.into(),
+        }
+    }
+}
+
+/// FFI-compatible MeltOptions
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+pub enum MeltOptions {
+    /// MPP (Multi-Part Payments) options
+    Mpp { amount: Amount },
+    /// Amountless options
+    Amountless { amount_msat: Amount },
+}
+
+impl From<MeltOptions> for cdk::nuts::MeltOptions {
+    fn from(opts: MeltOptions) -> Self {
+        match opts {
+            MeltOptions::Mpp { amount } => {
+                let cdk_amount: cdk::Amount = amount.into();
+                cdk::nuts::MeltOptions::new_mpp(cdk_amount)
+            }
+            MeltOptions::Amountless { amount_msat } => {
+                let cdk_amount: cdk::Amount = amount_msat.into();
+                cdk::nuts::MeltOptions::new_amountless(cdk_amount)
+            }
+        }
+    }
+}
+
+impl From<cdk::nuts::MeltOptions> for MeltOptions {
+    fn from(opts: cdk::nuts::MeltOptions) -> Self {
+        match opts {
+            cdk::nuts::MeltOptions::Mpp { mpp } => MeltOptions::Mpp {
+                amount: mpp.amount.into(),
+            },
+            cdk::nuts::MeltOptions::Amountless { amountless } => MeltOptions::Amountless {
+                amount_msat: amountless.amount_msat.into(),
+            },
+        }
+    }
+}
+
+/// FFI-compatible MintVersion
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MintVersion {
+    /// Mint Software name
+    pub name: String,
+    /// Mint Version
+    pub version: String,
+}
+
+impl From<cdk::nuts::MintVersion> for MintVersion {
+    fn from(version: cdk::nuts::MintVersion) -> Self {
+        Self {
+            name: version.name,
+            version: version.version,
+        }
+    }
+}
+
+impl From<MintVersion> for cdk::nuts::MintVersion {
+    fn from(version: MintVersion) -> Self {
+        Self {
+            name: version.name,
+            version: version.version,
+        }
+    }
+}
+
+impl MintVersion {
+    /// Convert MintVersion to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MintVersion from JSON string
+#[uniffi::export]
+pub fn decode_mint_version(json: String) -> Result<MintVersion, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode MintVersion to JSON string
+#[uniffi::export]
+pub fn encode_mint_version(version: MintVersion) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&version)?)
+}
+
+/// FFI-compatible ContactInfo
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ContactInfo {
+    /// Contact Method i.e. nostr
+    pub method: String,
+    /// Contact info i.e. npub...
+    pub info: String,
+}
+
+impl From<cdk::nuts::ContactInfo> for ContactInfo {
+    fn from(contact: cdk::nuts::ContactInfo) -> Self {
+        Self {
+            method: contact.method,
+            info: contact.info,
+        }
+    }
+}
+
+impl From<ContactInfo> for cdk::nuts::ContactInfo {
+    fn from(contact: ContactInfo) -> Self {
+        Self {
+            method: contact.method,
+            info: contact.info,
+        }
+    }
+}
+
+impl ContactInfo {
+    /// Convert ContactInfo to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ContactInfo from JSON string
+#[uniffi::export]
+pub fn decode_contact_info(json: String) -> Result<ContactInfo, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ContactInfo to JSON string
+#[uniffi::export]
+pub fn encode_contact_info(info: ContactInfo) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&info)?)
+}
+
+/// FFI-compatible SupportedSettings
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct SupportedSettings {
+    /// Setting supported
+    pub supported: bool,
+}
+
+impl From<cdk::nuts::nut06::SupportedSettings> for SupportedSettings {
+    fn from(settings: cdk::nuts::nut06::SupportedSettings) -> Self {
+        Self {
+            supported: settings.supported,
+        }
+    }
+}
+
+impl From<SupportedSettings> for cdk::nuts::nut06::SupportedSettings {
+    fn from(settings: SupportedSettings) -> Self {
+        Self {
+            supported: settings.supported,
+        }
+    }
+}
+
+/// FFI-compatible Nuts settings (simplified - only includes basic boolean flags)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Nuts {
+    /// NUT07 Settings - Token state check
+    pub nut07_supported: bool,
+    /// NUT08 Settings - Lightning fee return
+    pub nut08_supported: bool,
+    /// NUT09 Settings - Restore signature
+    pub nut09_supported: bool,
+    /// NUT10 Settings - Spending conditions
+    pub nut10_supported: bool,
+    /// NUT11 Settings - Pay to Public Key Hash
+    pub nut11_supported: bool,
+    /// NUT12 Settings - DLEQ proofs
+    pub nut12_supported: bool,
+    /// NUT14 Settings - Hashed Time Locked Contracts
+    pub nut14_supported: bool,
+    /// NUT20 Settings - Web sockets
+    pub nut20_supported: bool,
+    /// Supported currency units for minting
+    pub mint_units: Vec<CurrencyUnit>,
+    /// Supported currency units for melting
+    pub melt_units: Vec<CurrencyUnit>,
+}
+
+impl From<cdk::nuts::Nuts> for Nuts {
+    fn from(nuts: cdk::nuts::Nuts) -> Self {
+        Self {
+            nut07_supported: nuts.nut07.supported,
+            nut08_supported: nuts.nut08.supported,
+            nut09_supported: nuts.nut09.supported,
+            nut10_supported: nuts.nut10.supported,
+            nut11_supported: nuts.nut11.supported,
+            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(),
+        }
+    }
+}
+
+impl Nuts {
+    /// Convert Nuts to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Nuts from JSON string
+#[uniffi::export]
+pub fn decode_nuts(json: String) -> Result<Nuts, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Nuts to JSON string
+#[uniffi::export]
+pub fn encode_nuts(nuts: Nuts) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&nuts)?)
+}
+
+/// FFI-compatible MintInfo
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MintInfo {
+    /// name of the mint and should be recognizable
+    pub name: Option<String>,
+    /// hex pubkey of the mint  
+    pub pubkey: Option<String>,
+    /// implementation name and the version running
+    pub version: Option<MintVersion>,
+    /// short description of the mint
+    pub description: Option<String>,
+    /// long description
+    pub description_long: Option<String>,
+    /// Contact info
+    pub contact: Option<Vec<ContactInfo>>,
+    /// shows which NUTs the mint supports
+    pub nuts: Nuts,
+    /// Mint's icon URL
+    pub icon_url: Option<String>,
+    /// Mint's endpoint URLs
+    pub urls: Option<Vec<String>>,
+    /// message of the day that the wallet must display to the user
+    pub motd: Option<String>,
+    /// server unix timestamp
+    pub time: Option<u64>,
+    /// terms of url service of the mint
+    pub tos_url: Option<String>,
+}
+
+impl From<cdk::nuts::MintInfo> for MintInfo {
+    fn from(info: cdk::nuts::MintInfo) -> Self {
+        Self {
+            name: info.name,
+            pubkey: info.pubkey.map(|p| p.to_string()),
+            version: info.version.map(Into::into),
+            description: info.description,
+            description_long: info.description_long,
+            contact: info
+                .contact
+                .map(|contacts| contacts.into_iter().map(Into::into).collect()),
+            nuts: info.nuts.into(),
+            icon_url: info.icon_url,
+            urls: info.urls,
+            motd: info.motd,
+            time: info.time,
+            tos_url: info.tos_url,
+        }
+    }
+}
+
+impl From<MintInfo> for cdk_common::nuts::MintInfo {
+    fn from(info: MintInfo) -> Self {
+        Self {
+            name: info.name,
+            pubkey: info.pubkey.and_then(|p| p.parse().ok()),
+            version: info.version.map(Into::into),
+            description: info.description,
+            description_long: info.description_long,
+            contact: info
+                .contact
+                .map(|contacts| contacts.into_iter().map(Into::into).collect()),
+            nuts: cdk_common::nuts::Nuts::default(), // Simplified conversion
+            icon_url: info.icon_url,
+            urls: info.urls,
+            motd: info.motd,
+            time: info.time,
+            tos_url: info.tos_url,
+        }
+    }
+}
+
+impl MintInfo {
+    /// Convert MintInfo to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MintInfo from JSON string
+#[uniffi::export]
+pub fn decode_mint_info(json: String) -> Result<MintInfo, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode MintInfo to JSON string
+#[uniffi::export]
+pub fn encode_mint_info(info: MintInfo) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&info)?)
+}
+
+/// FFI-compatible Conditions (for spending conditions)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Conditions {
+    /// Unix locktime after which refund keys can be used
+    pub locktime: Option<u64>,
+    /// Additional Public keys (as hex strings)
+    pub pubkeys: Vec<String>,
+    /// Refund keys (as hex strings)
+    pub refund_keys: Vec<String>,
+    /// Number of signatures required (default 1)
+    pub num_sigs: Option<u64>,
+    /// Signature flag (0 = SigInputs, 1 = SigAll)
+    pub sig_flag: u8,
+    /// Number of refund signatures required (default 1)
+    pub num_sigs_refund: Option<u64>,
+}
+
+impl From<cdk::nuts::nut11::Conditions> for Conditions {
+    fn from(conditions: cdk::nuts::nut11::Conditions) -> Self {
+        Self {
+            locktime: conditions.locktime,
+            pubkeys: conditions
+                .pubkeys
+                .unwrap_or_default()
+                .into_iter()
+                .map(|p| p.to_string())
+                .collect(),
+            refund_keys: conditions
+                .refund_keys
+                .unwrap_or_default()
+                .into_iter()
+                .map(|p| p.to_string())
+                .collect(),
+            num_sigs: conditions.num_sigs,
+            sig_flag: match conditions.sig_flag {
+                cdk::nuts::nut11::SigFlag::SigInputs => 0,
+                cdk::nuts::nut11::SigFlag::SigAll => 1,
+            },
+            num_sigs_refund: conditions.num_sigs_refund,
+        }
+    }
+}
+
+impl TryFrom<Conditions> for cdk::nuts::nut11::Conditions {
+    type Error = FfiError;
+
+    fn try_from(conditions: Conditions) -> Result<Self, Self::Error> {
+        let pubkeys = if conditions.pubkeys.is_empty() {
+            None
+        } else {
+            Some(
+                conditions
+                    .pubkeys
+                    .into_iter()
+                    .map(|s| {
+                        s.parse().map_err(|e| FfiError::InvalidCryptographicKey {
+                            msg: format!("Invalid pubkey: {}", e),
+                        })
+                    })
+                    .collect::<Result<Vec<_>, _>>()?,
+            )
+        };
+
+        let refund_keys = if conditions.refund_keys.is_empty() {
+            None
+        } else {
+            Some(
+                conditions
+                    .refund_keys
+                    .into_iter()
+                    .map(|s| {
+                        s.parse().map_err(|e| FfiError::InvalidCryptographicKey {
+                            msg: format!("Invalid refund key: {}", e),
+                        })
+                    })
+                    .collect::<Result<Vec<_>, _>>()?,
+            )
+        };
+
+        let sig_flag = match conditions.sig_flag {
+            0 => cdk::nuts::nut11::SigFlag::SigInputs,
+            1 => cdk::nuts::nut11::SigFlag::SigAll,
+            _ => {
+                return Err(FfiError::Generic {
+                    msg: "Invalid sig_flag value".to_string(),
+                })
+            }
+        };
+
+        Ok(Self {
+            locktime: conditions.locktime,
+            pubkeys,
+            refund_keys,
+            num_sigs: conditions.num_sigs,
+            sig_flag,
+            num_sigs_refund: conditions.num_sigs_refund,
+        })
+    }
+}
+
+impl Conditions {
+    /// Convert Conditions to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Conditions from JSON string
+#[uniffi::export]
+pub fn decode_conditions(json: String) -> Result<Conditions, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Conditions to JSON string
+#[uniffi::export]
+pub fn encode_conditions(conditions: Conditions) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&conditions)?)
+}
+
+/// FFI-compatible Witness
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+pub enum Witness {
+    /// P2PK Witness
+    P2PK {
+        /// Signatures
+        signatures: Vec<String>,
+    },
+    /// HTLC Witness  
+    HTLC {
+        /// Preimage
+        preimage: String,
+        /// Optional signatures
+        signatures: Option<Vec<String>>,
+    },
+}
+
+impl From<cdk::nuts::Witness> for Witness {
+    fn from(witness: cdk::nuts::Witness) -> Self {
+        match witness {
+            cdk::nuts::Witness::P2PKWitness(p2pk) => Self::P2PK {
+                signatures: p2pk.signatures,
+            },
+            cdk::nuts::Witness::HTLCWitness(htlc) => Self::HTLC {
+                preimage: htlc.preimage,
+                signatures: htlc.signatures,
+            },
+        }
+    }
+}
+
+impl From<Witness> for cdk::nuts::Witness {
+    fn from(witness: Witness) -> Self {
+        match witness {
+            Witness::P2PK { signatures } => {
+                Self::P2PKWitness(cdk::nuts::nut11::P2PKWitness { signatures })
+            }
+            Witness::HTLC {
+                preimage,
+                signatures,
+            } => Self::HTLCWitness(cdk::nuts::nut14::HTLCWitness {
+                preimage,
+                signatures,
+            }),
+        }
+    }
+}
+
+/// FFI-compatible SpendingConditions
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
+pub enum SpendingConditions {
+    /// P2PK (Pay to Public Key) conditions
+    P2PK {
+        /// The public key (as hex string)
+        pubkey: String,
+        /// Additional conditions
+        conditions: Option<Conditions>,
+    },
+    /// HTLC (Hash Time Locked Contract) conditions
+    HTLC {
+        /// Hash of the preimage (as hex string)
+        hash: String,
+        /// Additional conditions
+        conditions: Option<Conditions>,
+    },
+}
+
+impl From<cdk::nuts::SpendingConditions> for SpendingConditions {
+    fn from(spending_conditions: cdk::nuts::SpendingConditions) -> Self {
+        match spending_conditions {
+            cdk::nuts::SpendingConditions::P2PKConditions { data, conditions } => Self::P2PK {
+                pubkey: data.to_string(),
+                conditions: conditions.map(Into::into),
+            },
+            cdk::nuts::SpendingConditions::HTLCConditions { data, conditions } => Self::HTLC {
+                hash: data.to_string(),
+                conditions: conditions.map(Into::into),
+            },
+        }
+    }
+}
+
+/// FFI-compatible Transaction
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Transaction {
+    /// Transaction ID
+    pub id: TransactionId,
+    /// Mint URL
+    pub mint_url: MintUrl,
+    /// Transaction direction
+    pub direction: TransactionDirection,
+    /// Amount
+    pub amount: Amount,
+    /// Fee
+    pub fee: Amount,
+    /// Currency Unit
+    pub unit: CurrencyUnit,
+    /// Proof Ys (Y values from proofs)
+    pub ys: Vec<PublicKey>,
+    /// Unix timestamp
+    pub timestamp: u64,
+    /// Memo
+    pub memo: Option<String>,
+    /// User-defined metadata
+    pub metadata: HashMap<String, String>,
+    /// Quote ID if this is a mint or melt transaction
+    pub quote_id: Option<String>,
+}
+
+impl From<cdk_common::wallet::Transaction> for Transaction {
+    fn from(tx: cdk_common::wallet::Transaction) -> Self {
+        Self {
+            id: tx.id().into(),
+            mint_url: tx.mint_url.into(),
+            direction: tx.direction.into(),
+            amount: tx.amount.into(),
+            fee: tx.fee.into(),
+            unit: tx.unit.into(),
+            ys: tx.ys.into_iter().map(Into::into).collect(),
+            timestamp: tx.timestamp,
+            memo: tx.memo,
+            metadata: tx.metadata,
+            quote_id: tx.quote_id,
+        }
+    }
+}
+
+/// Convert FFI Transaction to CDK Transaction
+impl TryFrom<Transaction> for cdk_common::wallet::Transaction {
+    type Error = FfiError;
+
+    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
+        let cdk_ys: Result<Vec<cdk_common::nuts::PublicKey>, _> =
+            tx.ys.into_iter().map(|pk| pk.try_into()).collect();
+        let cdk_ys = cdk_ys?;
+
+        Ok(Self {
+            mint_url: tx.mint_url.try_into()?,
+            direction: tx.direction.into(),
+            amount: tx.amount.into(),
+            fee: tx.fee.into(),
+            unit: tx.unit.into(),
+            ys: cdk_ys,
+            timestamp: tx.timestamp,
+            memo: tx.memo,
+            metadata: tx.metadata,
+            quote_id: tx.quote_id,
+        })
+    }
+}
+
+impl Transaction {
+    /// Convert Transaction to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Transaction from JSON string
+#[uniffi::export]
+pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Transaction to JSON string
+#[uniffi::export]
+pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&transaction)?)
+}
+
+/// FFI-compatible TransactionDirection
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum TransactionDirection {
+    /// Incoming transaction (i.e., receive or mint)
+    Incoming,
+    /// Outgoing transaction (i.e., send or melt)
+    Outgoing,
+}
+
+impl From<cdk_common::wallet::TransactionDirection> for TransactionDirection {
+    fn from(direction: cdk_common::wallet::TransactionDirection) -> Self {
+        match direction {
+            cdk_common::wallet::TransactionDirection::Incoming => TransactionDirection::Incoming,
+            cdk_common::wallet::TransactionDirection::Outgoing => TransactionDirection::Outgoing,
+        }
+    }
+}
+
+impl From<TransactionDirection> for cdk_common::wallet::TransactionDirection {
+    fn from(direction: TransactionDirection) -> Self {
+        match direction {
+            TransactionDirection::Incoming => cdk_common::wallet::TransactionDirection::Incoming,
+            TransactionDirection::Outgoing => cdk_common::wallet::TransactionDirection::Outgoing,
+        }
+    }
+}
+
+/// FFI-compatible TransactionId
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct TransactionId {
+    /// Hex-encoded transaction ID (64 characters)
+    pub hex: String,
+}
+
+impl TransactionId {
+    /// Create a new TransactionId from hex string
+    pub fn from_hex(hex: String) -> Result<Self, FfiError> {
+        // Validate hex string length (should be 64 characters for 32 bytes)
+        if hex.len() != 64 {
+            return Err(FfiError::InvalidHex {
+                msg: "Transaction ID hex must be exactly 64 characters (32 bytes)".to_string(),
+            });
+        }
+
+        // Validate hex format
+        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
+            return Err(FfiError::InvalidHex {
+                msg: "Transaction ID hex contains invalid characters".to_string(),
+            });
+        }
+
+        Ok(Self { hex })
+    }
+
+    /// Create from proofs
+    pub fn from_proofs(proofs: &Proofs) -> Result<Self, FfiError> {
+        let cdk_proofs: Vec<cdk::nuts::Proof> = proofs.iter().map(|p| p.inner.clone()).collect();
+        let id = cdk_common::wallet::TransactionId::from_proofs(cdk_proofs)?;
+        Ok(Self {
+            hex: id.to_string(),
+        })
+    }
+}
+
+impl From<cdk_common::wallet::TransactionId> for TransactionId {
+    fn from(id: cdk_common::wallet::TransactionId) -> Self {
+        Self {
+            hex: id.to_string(),
+        }
+    }
+}
+
+impl TryFrom<TransactionId> for cdk_common::wallet::TransactionId {
+    type Error = FfiError;
+
+    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
+        cdk_common::wallet::TransactionId::from_hex(&id.hex)
+            .map_err(|e| FfiError::InvalidHex { msg: e.to_string() })
+    }
+}
+
+/// FFI-compatible AuthProof
+#[cfg(feature = "auth")]
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct AuthProof {
+    /// Keyset ID
+    pub keyset_id: String,
+    /// Secret message
+    pub secret: String,
+    /// Unblinded signature (C)
+    pub c: String,
+    /// Y value (hash_to_curve of secret)
+    pub y: String,
+}
+
+#[cfg(feature = "auth")]
+impl From<cdk_common::AuthProof> for AuthProof {
+    fn from(auth_proof: cdk_common::AuthProof) -> Self {
+        Self {
+            keyset_id: auth_proof.keyset_id.to_string(),
+            secret: auth_proof.secret.to_string(),
+            c: auth_proof.c.to_string(),
+            y: auth_proof
+                .y()
+                .map(|y| y.to_string())
+                .unwrap_or_else(|_| "".to_string()),
+        }
+    }
+}
+
+#[cfg(feature = "auth")]
+impl TryFrom<AuthProof> for cdk_common::AuthProof {
+    type Error = FfiError;
+
+    fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
+        use std::str::FromStr;
+        Ok(Self {
+            keyset_id: cdk_common::Id::from_str(&auth_proof.keyset_id)
+                .map_err(|e| FfiError::Serialization { msg: e.to_string() })?,
+            secret: {
+                use std::str::FromStr;
+                cdk_common::secret::Secret::from_str(&auth_proof.secret)
+                    .map_err(|e| FfiError::Serialization { msg: e.to_string() })?
+            },
+            c: cdk_common::PublicKey::from_str(&auth_proof.c)
+                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?,
+            dleq: None, // FFI doesn't expose DLEQ proofs for simplicity
+        })
+    }
+}
+
+#[cfg(feature = "auth")]
+impl AuthProof {
+    /// Convert AuthProof to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode AuthProof from JSON string
+#[cfg(feature = "auth")]
+#[uniffi::export]
+pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode AuthProof to JSON string
+#[cfg(feature = "auth")]
+#[uniffi::export]
+pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&proof)?)
+}
+
+impl TryFrom<SpendingConditions> for cdk::nuts::SpendingConditions {
+    type Error = FfiError;
+
+    fn try_from(spending_conditions: SpendingConditions) -> Result<Self, Self::Error> {
+        match spending_conditions {
+            SpendingConditions::P2PK { pubkey, conditions } => {
+                let pubkey = pubkey
+                    .parse()
+                    .map_err(|e| FfiError::InvalidCryptographicKey {
+                        msg: format!("Invalid pubkey: {}", e),
+                    })?;
+                let conditions = conditions.map(|c| c.try_into()).transpose()?;
+                Ok(Self::P2PKConditions {
+                    data: pubkey,
+                    conditions,
+                })
+            }
+            SpendingConditions::HTLC { hash, conditions } => {
+                let hash = hash
+                    .parse()
+                    .map_err(|e| FfiError::InvalidCryptographicKey {
+                        msg: format!("Invalid hash: {}", e),
+                    })?;
+                let conditions = conditions.map(|c| c.try_into()).transpose()?;
+                Ok(Self::HTLCConditions {
+                    data: hash,
+                    conditions,
+                })
+            }
+        }
+    }
+}
+
+/// FFI-compatible SubscriptionKind
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum SubscriptionKind {
+    /// Bolt 11 Melt Quote
+    Bolt11MeltQuote,
+    /// Bolt 11 Mint Quote
+    Bolt11MintQuote,
+    /// Bolt 12 Mint Quote
+    Bolt12MintQuote,
+    /// Proof State
+    ProofState,
+}
+
+impl From<SubscriptionKind> for cdk::nuts::nut17::Kind {
+    fn from(kind: SubscriptionKind) -> Self {
+        match kind {
+            SubscriptionKind::Bolt11MeltQuote => cdk::nuts::nut17::Kind::Bolt11MeltQuote,
+            SubscriptionKind::Bolt11MintQuote => cdk::nuts::nut17::Kind::Bolt11MintQuote,
+            SubscriptionKind::Bolt12MintQuote => cdk::nuts::nut17::Kind::Bolt12MintQuote,
+            SubscriptionKind::ProofState => cdk::nuts::nut17::Kind::ProofState,
+        }
+    }
+}
+
+impl From<cdk::nuts::nut17::Kind> for SubscriptionKind {
+    fn from(kind: cdk::nuts::nut17::Kind) -> Self {
+        match kind {
+            cdk::nuts::nut17::Kind::Bolt11MeltQuote => SubscriptionKind::Bolt11MeltQuote,
+            cdk::nuts::nut17::Kind::Bolt11MintQuote => SubscriptionKind::Bolt11MintQuote,
+            cdk::nuts::nut17::Kind::Bolt12MintQuote => SubscriptionKind::Bolt12MintQuote,
+            cdk::nuts::nut17::Kind::ProofState => SubscriptionKind::ProofState,
+        }
+    }
+}
+
+/// FFI-compatible SubscribeParams
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct SubscribeParams {
+    /// Subscription kind
+    pub kind: SubscriptionKind,
+    /// Filters
+    pub filters: Vec<String>,
+    /// Subscription ID (optional, will be generated if not provided)
+    pub id: Option<String>,
+}
+
+impl From<SubscribeParams> for cdk_common::subscription::Params {
+    fn from(params: SubscribeParams) -> Self {
+        let sub_id = params
+            .id
+            .map(|id| SubId::from(id.as_str()))
+            .unwrap_or_else(|| {
+                // Generate a random ID
+                let uuid = uuid::Uuid::new_v4();
+                SubId::from(uuid.to_string().as_str())
+            });
+
+        cdk::nuts::nut17::Params {
+            kind: params.kind.into(),
+            filters: params.filters,
+            id: sub_id,
+        }
+    }
+}
+
+impl SubscribeParams {
+    /// Convert SubscribeParams to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode SubscribeParams from JSON string
+#[uniffi::export]
+pub fn decode_subscribe_params(json: String) -> Result<SubscribeParams, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode SubscribeParams to JSON string
+#[uniffi::export]
+pub fn encode_subscribe_params(params: SubscribeParams) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&params)?)
+}
+
+/// FFI-compatible ActiveSubscription
+#[derive(uniffi::Object)]
+pub struct ActiveSubscription {
+    inner: std::sync::Arc<tokio::sync::Mutex<cdk::wallet::subscription::ActiveSubscription>>,
+    pub sub_id: String,
+}
+
+impl ActiveSubscription {
+    pub(crate) fn new(
+        inner: cdk::wallet::subscription::ActiveSubscription,
+        sub_id: String,
+    ) -> Self {
+        Self {
+            inner: std::sync::Arc::new(tokio::sync::Mutex::new(inner)),
+            sub_id,
+        }
+    }
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+impl ActiveSubscription {
+    /// Get the subscription ID
+    pub fn id(&self) -> String {
+        self.sub_id.clone()
+    }
+
+    /// Receive the next notification
+    pub async fn recv(&self) -> Result<NotificationPayload, FfiError> {
+        let mut guard = self.inner.lock().await;
+        guard
+            .recv()
+            .await
+            .ok_or(FfiError::Generic {
+                msg: "Subscription closed".to_string(),
+            })
+            .map(Into::into)
+    }
+
+    /// Try to receive a notification without blocking
+    pub async fn try_recv(&self) -> Result<Option<NotificationPayload>, FfiError> {
+        let mut guard = self.inner.lock().await;
+        guard
+            .try_recv()
+            .map(|opt| opt.map(Into::into))
+            .map_err(|e| FfiError::Generic {
+                msg: format!("Failed to receive notification: {}", e),
+            })
+    }
+}
+
+/// FFI-compatible NotificationPayload
+#[derive(Debug, Clone, uniffi::Enum)]
+pub enum NotificationPayload {
+    /// Proof state update
+    ProofState { proof_states: Vec<ProofStateUpdate> },
+    /// Mint quote update
+    MintQuoteUpdate {
+        quote: std::sync::Arc<MintQuoteBolt11Response>,
+    },
+    /// Melt quote update
+    MeltQuoteUpdate {
+        quote: std::sync::Arc<MeltQuoteBolt11Response>,
+    },
+}
+
+impl From<cdk::nuts::NotificationPayload<String>> for NotificationPayload {
+    fn from(payload: cdk::nuts::NotificationPayload<String>) -> Self {
+        match payload {
+            cdk::nuts::NotificationPayload::ProofState(states) => NotificationPayload::ProofState {
+                proof_states: vec![states.into()],
+            },
+            cdk::nuts::NotificationPayload::MintQuoteBolt11Response(quote_resp) => {
+                NotificationPayload::MintQuoteUpdate {
+                    quote: std::sync::Arc::new(quote_resp.into()),
+                }
+            }
+            cdk::nuts::NotificationPayload::MeltQuoteBolt11Response(quote_resp) => {
+                NotificationPayload::MeltQuoteUpdate {
+                    quote: std::sync::Arc::new(quote_resp.into()),
+                }
+            }
+            _ => {
+                // For now, handle other notification types as empty ProofState
+                NotificationPayload::ProofState {
+                    proof_states: vec![],
+                }
+            }
+        }
+    }
+}
+
+/// FFI-compatible ProofStateUpdate
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ProofStateUpdate {
+    /// Y value (hash_to_curve of secret)
+    pub y: String,
+    /// Current state
+    pub state: ProofState,
+    /// Optional witness data
+    pub witness: Option<String>,
+}
+
+impl From<cdk::nuts::nut07::ProofState> for ProofStateUpdate {
+    fn from(proof_state: cdk::nuts::nut07::ProofState) -> Self {
+        Self {
+            y: proof_state.y.to_string(),
+            state: proof_state.state.into(),
+            witness: proof_state.witness.map(|w| format!("{:?}", w)),
+        }
+    }
+}
+
+impl ProofStateUpdate {
+    /// Convert ProofStateUpdate to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ProofStateUpdate from JSON string
+#[uniffi::export]
+pub fn decode_proof_state_update(json: String) -> Result<ProofStateUpdate, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ProofStateUpdate to JSON string
+#[uniffi::export]
+pub fn encode_proof_state_update(update: ProofStateUpdate) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&update)?)
+}
+
+/// FFI-compatible KeySetInfo
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct KeySetInfo {
+    pub id: String,
+    pub unit: CurrencyUnit,
+    pub active: bool,
+    /// Input fee per thousand (ppk)
+    pub input_fee_ppk: u64,
+}
+
+impl From<cdk_common::nuts::KeySetInfo> for KeySetInfo {
+    fn from(keyset: cdk_common::nuts::KeySetInfo) -> Self {
+        Self {
+            id: keyset.id.to_string(),
+            unit: keyset.unit.into(),
+            active: keyset.active,
+            input_fee_ppk: keyset.input_fee_ppk,
+        }
+    }
+}
+
+impl From<KeySetInfo> for cdk_common::nuts::KeySetInfo {
+    fn from(keyset: KeySetInfo) -> Self {
+        use std::str::FromStr;
+        Self {
+            id: cdk_common::nuts::Id::from_str(&keyset.id).unwrap_or_else(|_| {
+                // Create a dummy keyset for empty mint keys
+                use std::collections::BTreeMap;
+                let empty_map = BTreeMap::new();
+                let empty_keys = cdk_common::nut01::MintKeys::new(empty_map);
+                cdk_common::nuts::Id::from(&empty_keys)
+            }),
+            unit: keyset.unit.into(),
+            active: keyset.active,
+            final_expiry: None,
+            input_fee_ppk: keyset.input_fee_ppk,
+        }
+    }
+}
+
+impl KeySetInfo {
+    /// Convert KeySetInfo to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode KeySetInfo from JSON string
+#[uniffi::export]
+pub fn decode_key_set_info(json: String) -> Result<KeySetInfo, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode KeySetInfo to JSON string
+#[uniffi::export]
+pub fn encode_key_set_info(info: KeySetInfo) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&info)?)
+}
+
+/// FFI-compatible PublicKey
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct PublicKey {
+    /// Hex-encoded public key
+    pub hex: String,
+}
+
+impl From<cdk_common::nuts::PublicKey> for PublicKey {
+    fn from(key: cdk_common::nuts::PublicKey) -> Self {
+        Self {
+            hex: key.to_string(),
+        }
+    }
+}
+
+impl TryFrom<PublicKey> for cdk_common::nuts::PublicKey {
+    type Error = FfiError;
+
+    fn try_from(key: PublicKey) -> Result<Self, Self::Error> {
+        key.hex
+            .parse()
+            .map_err(|e| FfiError::InvalidCryptographicKey {
+                msg: format!("Invalid public key: {}", e),
+            })
+    }
+}
+
+/// FFI-compatible Keys (simplified - contains only essential info)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Keys {
+    /// Keyset ID
+    pub id: String,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Map of amount to public key hex (simplified from BTreeMap)
+    pub keys: HashMap<u64, String>,
+}
+
+impl From<cdk_common::nuts::Keys> for Keys {
+    fn from(keys: cdk_common::nuts::Keys) -> Self {
+        // Keys doesn't have id and unit - we'll need to get these from context
+        // For now, use placeholder values
+        Self {
+            id: "unknown".to_string(), // This should come from KeySet
+            unit: CurrencyUnit::Sat,   // This should come from KeySet
+            keys: keys
+                .keys()
+                .iter()
+                .map(|(amount, pubkey)| (u64::from(*amount), pubkey.to_string()))
+                .collect(),
+        }
+    }
+}
+
+impl TryFrom<Keys> for cdk_common::nuts::Keys {
+    type Error = FfiError;
+
+    fn try_from(keys: Keys) -> Result<Self, Self::Error> {
+        use std::collections::BTreeMap;
+        use std::str::FromStr;
+
+        // Convert the HashMap to BTreeMap with proper types
+        let mut keys_map = BTreeMap::new();
+        for (amount_u64, pubkey_hex) in keys.keys {
+            let amount = cashu::Amount::from(amount_u64);
+            let pubkey = cashu::PublicKey::from_str(&pubkey_hex)
+                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
+            keys_map.insert(amount, pubkey);
+        }
+
+        Ok(cdk_common::nuts::Keys::new(keys_map))
+    }
+}
+
+impl Keys {
+    /// Convert Keys to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Keys from JSON string
+#[uniffi::export]
+pub fn decode_keys(json: String) -> Result<Keys, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Keys to JSON string
+#[uniffi::export]
+pub fn encode_keys(keys: Keys) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&keys)?)
+}
+
+/// FFI-compatible KeySet
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct KeySet {
+    /// Keyset ID
+    pub id: String,
+    /// Currency unit  
+    pub unit: CurrencyUnit,
+    /// The keys (map of amount to public key hex)
+    pub keys: HashMap<u64, String>,
+    /// Optional expiry timestamp
+    pub final_expiry: Option<u64>,
+}
+
+impl From<cashu::KeySet> for KeySet {
+    fn from(keyset: cashu::KeySet) -> Self {
+        Self {
+            id: keyset.id.to_string(),
+            unit: keyset.unit.into(),
+            keys: keyset
+                .keys
+                .keys()
+                .iter()
+                .map(|(amount, pubkey)| (u64::from(*amount), pubkey.to_string()))
+                .collect(),
+            final_expiry: keyset.final_expiry,
+        }
+    }
+}
+
+impl TryFrom<KeySet> for cashu::KeySet {
+    type Error = FfiError;
+
+    fn try_from(keyset: KeySet) -> Result<Self, Self::Error> {
+        use std::collections::BTreeMap;
+        use std::str::FromStr;
+
+        // Convert id
+        let id = cashu::Id::from_str(&keyset.id)
+            .map_err(|e| FfiError::Serialization { msg: e.to_string() })?;
+
+        // Convert unit
+        let unit: cashu::CurrencyUnit = keyset.unit.into();
+
+        // Convert keys
+        let mut keys_map = BTreeMap::new();
+        for (amount_u64, pubkey_hex) in keyset.keys {
+            let amount = cashu::Amount::from(amount_u64);
+            let pubkey = cashu::PublicKey::from_str(&pubkey_hex)
+                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
+            keys_map.insert(amount, pubkey);
+        }
+        let keys = cashu::Keys::new(keys_map);
+
+        Ok(cashu::KeySet {
+            id,
+            unit,
+            keys,
+            final_expiry: keyset.final_expiry,
+        })
+    }
+}
+
+impl KeySet {
+    /// Convert KeySet to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode KeySet from JSON string
+#[uniffi::export]
+pub fn decode_key_set(json: String) -> Result<KeySet, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode KeySet to JSON string
+#[uniffi::export]
+pub fn encode_key_set(keyset: KeySet) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&keyset)?)
+}
+
+/// FFI-compatible ProofInfo
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct ProofInfo {
+    /// Proof
+    pub proof: std::sync::Arc<Proof>,
+    /// Y value (hash_to_curve of secret)
+    pub y: PublicKey,
+    /// Mint URL
+    pub mint_url: MintUrl,
+    /// Proof state
+    pub state: ProofState,
+    /// Proof Spending Conditions
+    pub spending_condition: Option<SpendingConditions>,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+}
+
+impl From<cdk_common::common::ProofInfo> for ProofInfo {
+    fn from(info: cdk_common::common::ProofInfo) -> Self {
+        Self {
+            proof: std::sync::Arc::new(info.proof.into()),
+            y: info.y.into(),
+            mint_url: info.mint_url.into(),
+            state: info.state.into(),
+            spending_condition: info.spending_condition.map(Into::into),
+            unit: info.unit.into(),
+        }
+    }
+}
+
+/// Decode ProofInfo from JSON string
+#[uniffi::export]
+pub fn decode_proof_info(json: String) -> Result<ProofInfo, FfiError> {
+    let info: cdk_common::common::ProofInfo = serde_json::from_str(&json)?;
+    Ok(info.into())
+}
+
+/// Encode ProofInfo to JSON string
+#[uniffi::export]
+pub fn encode_proof_info(info: ProofInfo) -> Result<String, FfiError> {
+    // Convert to cdk_common::common::ProofInfo for serialization
+    let cdk_info = cdk_common::common::ProofInfo {
+        proof: info.proof.inner.clone(),
+        y: info.y.try_into()?,
+        mint_url: info.mint_url.try_into()?,
+        state: info.state.into(),
+        spending_condition: info.spending_condition.and_then(|c| c.try_into().ok()),
+        unit: info.unit.into(),
+    };
+    Ok(serde_json::to_string(&cdk_info)?)
+}
+
+// State enum removed - using ProofState instead
+
+/// FFI-compatible Id (for keyset IDs)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct Id {
+    pub hex: String,
+}
+
+impl From<cdk_common::nuts::Id> for Id {
+    fn from(id: cdk_common::nuts::Id) -> Self {
+        Self {
+            hex: id.to_string(),
+        }
+    }
+}
+
+impl From<Id> for cdk_common::nuts::Id {
+    fn from(id: Id) -> Self {
+        use std::str::FromStr;
+        Self::from_str(&id.hex).unwrap_or_else(|_| {
+            // Create a dummy keyset for empty mint keys
+            use std::collections::BTreeMap;
+            let empty_map = BTreeMap::new();
+            let empty_keys = cdk_common::nut01::MintKeys::new(empty_map);
+            cdk_common::nuts::Id::from(&empty_keys)
+        })
+    }
+}

+ 486 - 0
crates/cdk-ffi/src/wallet.rs

@@ -0,0 +1,486 @@
+//! FFI Wallet bindings
+
+use std::str::FromStr;
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
+
+use crate::error::FfiError;
+use crate::types::*;
+
+/// FFI-compatible Wallet
+#[derive(uniffi::Object)]
+pub struct Wallet {
+    inner: Arc<CdkWallet>,
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+impl Wallet {
+    /// Create a new Wallet from mnemonic using WalletDatabase trait
+    #[uniffi::constructor]
+    pub async fn new(
+        mint_url: String,
+        unit: CurrencyUnit,
+        mnemonic: String,
+        db: Arc<dyn crate::database::WalletDatabase>,
+        config: WalletConfig,
+    ) -> Result<Self, FfiError> {
+        // Parse mnemonic and generate seed without passphrase
+        let m = Mnemonic::parse(&mnemonic)
+            .map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
+        let seed = m.to_seed_normalized("");
+
+        // Convert the FFI database trait to a CDK database implementation
+        let localstore = crate::database::create_cdk_database_from_ffi(db);
+
+        let wallet =
+            CdkWalletBuilder::new()
+                .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
+                    FfiError::InvalidUrl { msg: e.to_string() }
+                })?)
+                .unit(unit.into())
+                .localstore(localstore)
+                .seed(seed)
+                .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
+                .build()
+                .map_err(FfiError::from)?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Get the mint URL
+    pub fn mint_url(&self) -> MintUrl {
+        self.inner.mint_url.clone().into()
+    }
+
+    /// Get the currency unit
+    pub fn unit(&self) -> CurrencyUnit {
+        self.inner.unit.clone().into()
+    }
+
+    /// Get total balance
+    pub async fn total_balance(&self) -> Result<Amount, FfiError> {
+        let balance = self.inner.total_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Get total pending balance
+    pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
+        let balance = self.inner.total_pending_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Get total reserved balance
+    pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
+        let balance = self.inner.total_reserved_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Get mint info
+    pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
+        let info = self.inner.fetch_mint_info().await?;
+        Ok(info.map(Into::into))
+    }
+
+    /// Receive tokens
+    pub async fn receive(
+        &self,
+        token: std::sync::Arc<Token>,
+        options: ReceiveOptions,
+    ) -> Result<Amount, FfiError> {
+        let amount = self
+            .inner
+            .receive(&token.to_string(), options.into())
+            .await?;
+        Ok(amount.into())
+    }
+
+    /// Restore wallet from seed
+    pub async fn restore(&self) -> Result<Amount, FfiError> {
+        let amount = self.inner.restore().await?;
+        Ok(amount.into())
+    }
+
+    /// Verify token DLEQ proofs
+    pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
+        let cdk_token = token.inner.clone();
+        self.inner.verify_token_dleq(&cdk_token).await?;
+        Ok(())
+    }
+
+    /// Receive proofs directly
+    pub async fn receive_proofs(
+        &self,
+        proofs: Proofs,
+        options: ReceiveOptions,
+        memo: Option<String>,
+    ) -> Result<Amount, FfiError> {
+        let cdk_proofs: Vec<cdk::nuts::Proof> =
+            proofs.into_iter().map(|p| p.inner.clone()).collect();
+
+        let amount = self
+            .inner
+            .receive_proofs(cdk_proofs, options.into(), memo)
+            .await?;
+        Ok(amount.into())
+    }
+
+    /// Prepare a send operation
+    pub async fn prepare_send(
+        &self,
+        amount: Amount,
+        options: SendOptions,
+    ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
+        let prepared = self
+            .inner
+            .prepare_send(amount.into(), options.into())
+            .await?;
+        Ok(std::sync::Arc::new(prepared.into()))
+    }
+
+    /// Get a mint quote
+    pub async fn mint_quote(
+        &self,
+        amount: Amount,
+        description: Option<String>,
+    ) -> Result<MintQuote, FfiError> {
+        let quote = self.inner.mint_quote(amount.into(), description).await?;
+        Ok(quote.into())
+    }
+
+    /// Mint tokens
+    pub async fn mint(
+        &self,
+        quote_id: String,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, FfiError> {
+        // Convert spending conditions if provided
+        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let proofs = self
+            .inner
+            .mint(&quote_id, amount_split_target.into(), conditions)
+            .await?;
+        Ok(proofs
+            .into_iter()
+            .map(|p| std::sync::Arc::new(p.into()))
+            .collect())
+    }
+
+    /// Get a melt quote
+    pub async fn melt_quote(
+        &self,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> Result<MeltQuote, FfiError> {
+        let cdk_options = options.map(Into::into);
+        let quote = self.inner.melt_quote(request, cdk_options).await?;
+        Ok(quote.into())
+    }
+
+    /// Melt tokens
+    pub async fn melt(&self, quote_id: String) -> Result<Melted, FfiError> {
+        let melted = self.inner.melt(&quote_id).await?;
+        Ok(melted.into())
+    }
+
+    /// Get a quote for a bolt12 mint
+    pub async fn mint_bolt12_quote(
+        &self,
+        amount: Option<Amount>,
+        description: Option<String>,
+    ) -> Result<MintQuote, FfiError> {
+        let quote = self
+            .inner
+            .mint_bolt12_quote(amount.map(Into::into), description)
+            .await?;
+        Ok(quote.into())
+    }
+
+    /// Mint tokens using bolt12
+    pub async fn mint_bolt12(
+        &self,
+        quote_id: String,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, FfiError> {
+        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let proofs = self
+            .inner
+            .mint_bolt12(
+                &quote_id,
+                amount.map(Into::into),
+                amount_split_target.into(),
+                conditions,
+            )
+            .await?;
+
+        Ok(proofs
+            .into_iter()
+            .map(|p| std::sync::Arc::new(p.into()))
+            .collect())
+    }
+
+    /// Get a quote for a bolt12 melt
+    pub async fn melt_bolt12_quote(
+        &self,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> Result<MeltQuote, FfiError> {
+        let cdk_options = options.map(Into::into);
+        let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?;
+        Ok(quote.into())
+    }
+
+    /// Swap proofs
+    pub async fn swap(
+        &self,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        input_proofs: Proofs,
+        spending_conditions: Option<SpendingConditions>,
+        include_fees: bool,
+    ) -> Result<Option<Proofs>, FfiError> {
+        let cdk_proofs: Vec<cdk::nuts::Proof> =
+            input_proofs.into_iter().map(|p| p.inner.clone()).collect();
+
+        // Convert spending conditions if provided
+        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let result = self
+            .inner
+            .swap(
+                amount.map(Into::into),
+                amount_split_target.into(),
+                cdk_proofs,
+                conditions,
+                include_fees,
+            )
+            .await?;
+
+        Ok(result.map(|proofs| {
+            proofs
+                .into_iter()
+                .map(|p| std::sync::Arc::new(p.into()))
+                .collect()
+        }))
+    }
+
+    /// Get proofs by states
+    pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
+        let mut all_proofs = Vec::new();
+
+        for state in states {
+            let proofs = match state {
+                ProofState::Unspent => self.inner.get_unspent_proofs().await?,
+                ProofState::Pending => self.inner.get_pending_proofs().await?,
+                ProofState::Reserved => self.inner.get_reserved_proofs().await?,
+                ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
+                ProofState::Spent => {
+                    // CDK doesn't have a method to get spent proofs directly
+                    // They are removed from the database when spent
+                    continue;
+                }
+            };
+
+            for proof in proofs {
+                all_proofs.push(std::sync::Arc::new(proof.into()));
+            }
+        }
+
+        Ok(all_proofs)
+    }
+
+    /// Check if proofs are spent
+    pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
+        let cdk_proofs: Vec<cdk::nuts::Proof> =
+            proofs.into_iter().map(|p| p.inner.clone()).collect();
+
+        let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
+        // Convert ProofState to bool (spent = true, unspent = false)
+        let spent_bools = proof_states
+            .into_iter()
+            .map(|proof_state| {
+                matches!(
+                    proof_state.state,
+                    cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
+                )
+            })
+            .collect();
+        Ok(spent_bools)
+    }
+
+    /// List transactions
+    pub async fn list_transactions(
+        &self,
+        direction: Option<TransactionDirection>,
+    ) -> Result<Vec<Transaction>, FfiError> {
+        let cdk_direction = direction.map(Into::into);
+        let transactions = self.inner.list_transactions(cdk_direction).await?;
+        Ok(transactions.into_iter().map(Into::into).collect())
+    }
+
+    /// Get transaction by ID
+    pub async fn get_transaction(
+        &self,
+        id: TransactionId,
+    ) -> Result<Option<Transaction>, FfiError> {
+        let cdk_id = id.try_into()?;
+        let transaction = self.inner.get_transaction(cdk_id).await?;
+        Ok(transaction.map(Into::into))
+    }
+
+    /// Revert a transaction
+    pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
+        let cdk_id = id.try_into()?;
+        self.inner.revert_transaction(cdk_id).await?;
+        Ok(())
+    }
+
+    /// Subscribe to wallet events
+    pub async fn subscribe(
+        &self,
+        params: SubscribeParams,
+    ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
+        let cdk_params: cdk_common::subscription::Params = params.clone().into();
+        let sub_id = cdk_params.id.to_string();
+        let active_sub = self.inner.subscribe(cdk_params).await;
+        Ok(std::sync::Arc::new(ActiveSubscription::new(
+            active_sub, sub_id,
+        )))
+    }
+
+    /// Refresh keysets from the mint
+    pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
+        let keysets = self.inner.refresh_keysets().await?;
+        Ok(keysets.into_iter().map(Into::into).collect())
+    }
+
+    /// Get the active keyset for the wallet's unit
+    pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
+        let keyset = self.inner.get_active_keyset().await?;
+        Ok(keyset.into())
+    }
+
+    /// Get fees for a specific keyset ID
+    pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
+        let id = cdk::nuts::Id::from_str(&keyset_id)
+            .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
+        let fees = self.inner.get_keyset_fees_by_id(id).await?;
+        Ok(fees)
+    }
+
+    /// Reclaim unspent proofs (mark them as unspent in the database)
+    pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), FfiError> {
+        let cdk_proofs: Vec<cdk::nuts::Proof> = proofs.iter().map(|p| p.inner.clone()).collect();
+        self.inner.reclaim_unspent(cdk_proofs).await?;
+        Ok(())
+    }
+
+    /// Check all pending proofs and return the total amount reclaimed
+    pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
+        let amount = self.inner.check_all_pending_proofs().await?;
+        Ok(amount.into())
+    }
+
+    /// Calculate fee for a given number of proofs with the specified keyset
+    pub async fn calculate_fee(
+        &self,
+        proof_count: u32,
+        keyset_id: String,
+    ) -> Result<Amount, FfiError> {
+        let id = cdk::nuts::Id::from_str(&keyset_id)
+            .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
+        let fee_ppk = self.inner.get_keyset_fees_by_id(id).await?;
+        let total_fee = (proof_count as u64 * fee_ppk) / 1000; // fee is per thousand
+        Ok(Amount::new(total_fee))
+    }
+}
+
+/// BIP353 methods for Wallet
+#[cfg(feature = "bip353")]
+#[uniffi::export(async_runtime = "tokio")]
+impl Wallet {
+    /// Get a quote for a BIP353 melt
+    ///
+    /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer
+    /// and then creates a melt quote for that offer.
+    pub async fn melt_bip353_quote(
+        &self,
+        bip353_address: String,
+        amount_msat: Amount,
+    ) -> Result<MeltQuote, FfiError> {
+        let cdk_amount: cdk::Amount = amount_msat.into();
+        let quote = self
+            .inner
+            .melt_bip353_quote(&bip353_address, cdk_amount)
+            .await?;
+        Ok(quote.into())
+    }
+}
+
+/// Auth methods for Wallet
+#[cfg(feature = "auth")]
+#[uniffi::export(async_runtime = "tokio")]
+impl Wallet {
+    /// Set Clear Auth Token (CAT) for authentication
+    pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
+        self.inner.set_cat(cat).await?;
+        Ok(())
+    }
+
+    /// Set refresh token for authentication
+    pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
+        self.inner.set_refresh_token(refresh_token).await?;
+        Ok(())
+    }
+
+    /// Refresh access token using the stored refresh token
+    pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
+        self.inner.refresh_access_token().await?;
+        Ok(())
+    }
+
+    /// Mint blind auth tokens
+    pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
+        let proofs = self.inner.mint_blind_auth(amount.into()).await?;
+        Ok(proofs
+            .into_iter()
+            .map(|p| std::sync::Arc::new(p.into()))
+            .collect())
+    }
+
+    /// Get unspent auth proofs
+    pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
+        let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
+        Ok(auth_proofs.into_iter().map(Into::into).collect())
+    }
+}
+
+/// Configuration for creating wallets
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct WalletConfig {
+    pub target_proof_count: Option<u32>,
+}
+
+/// Generates a new random mnemonic phrase
+#[uniffi::export]
+pub fn generate_mnemonic() -> Result<String, FfiError> {
+    let mnemonic =
+        Mnemonic::generate(12).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
+    Ok(mnemonic.to_string())
+}
+
+/// Converts a mnemonic phrase to its entropy bytes
+#[uniffi::export]
+pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
+    let m =
+        Mnemonic::parse(&mnemonic).map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
+    Ok(m.to_entropy())
+}

+ 10 - 0
crates/cdk-ffi/uniffi.toml

@@ -0,0 +1,10 @@
+[bindings.kotlin]
+package_name = "org.cashudevkit"
+cdylib_name = "cdk_ffi"
+
+[bindings.python]
+cdylib_name = "cdk_ffi"
+
+[bindings.swift]
+module_name = "CashuDevKit"
+cdylib_name = "cdk_ffi"

+ 151 - 0
justfile

@@ -348,6 +348,13 @@ release m="":
     echo
   done
 
+  # Extract version from the cdk-ffi crate
+  VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "cdk-ffi") | .version')
+  
+  # Trigger Swift package release after Rust crates are published
+  echo "📦 Triggering Swift package release for version $VERSION..."
+  just ffi-release-swift $VERSION
+
 check-docs:
   #!/usr/bin/env bash
   set -euo pipefail
@@ -404,3 +411,147 @@ docs-strict:
     RUSTDOCFLAGS="-D warnings" cargo doc $arg --all-features --no-deps
     echo
   done
+
+# =============================================================================
+# FFI Commands - CDK Foreign Function Interface bindings
+# =============================================================================
+
+# Helper function to get library extension based on platform
+_ffi-lib-ext:
+  #!/usr/bin/env bash
+  if [[ "$OSTYPE" == "darwin"* ]]; then
+    echo "dylib"
+  else
+    echo "so"
+  fi
+
+# Build the FFI library
+ffi-build *ARGS="--release":
+  cargo build {{ARGS}} --package cdk-ffi
+
+# Generate bindings for a specific language
+ffi-generate LANGUAGE *ARGS="--release": ffi-build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  LANG="{{LANGUAGE}}"
+  
+  # Validate language
+  case "$LANG" in
+    python|swift|kotlin)
+      ;;
+    *)
+      echo "❌ Unsupported language: $LANG"
+      echo "Supported languages: python, swift, kotlin"
+      exit 1
+      ;;
+  esac
+  
+  # Set emoji and build type
+  case "$LANG" in
+    python) EMOJI="🐍" ;;
+    swift) EMOJI="🍎" ;;
+    kotlin) EMOJI="🎯" ;;
+  esac
+  
+  # Determine build type and library path
+  if [[ "{{ARGS}}" == *"--release"* ]] || [[ "{{ARGS}}" == "" ]]; then
+    BUILD_TYPE="release"
+  else
+    BUILD_TYPE="debug"
+    cargo build --package cdk-ffi
+  fi
+  
+  LIB_EXT=$(just _ffi-lib-ext)
+  
+  echo "$EMOJI Generating $LANG bindings..."
+  mkdir -p target/bindings/$LANG
+  
+  cargo run --bin uniffi-bindgen generate \
+    --library target/$BUILD_TYPE/libcdk_ffi.$LIB_EXT \
+    --language $LANG \
+    --out-dir target/bindings/$LANG
+  
+  echo "✅ $LANG bindings generated in target/bindings/$LANG/"
+
+# Generate Python bindings (shorthand)
+ffi-generate-python *ARGS="--release": 
+  just ffi-generate python {{ARGS}}
+
+# Generate Swift bindings (shorthand)
+ffi-generate-swift *ARGS="--release":
+  just ffi-generate swift {{ARGS}}
+
+# Generate Kotlin bindings (shorthand)
+ffi-generate-kotlin *ARGS="--release":
+  just ffi-generate kotlin {{ARGS}}
+
+# Generate bindings for all supported languages
+ffi-generate-all *ARGS="--release": ffi-build
+  @echo "🔧 Generating UniFFI bindings for all languages..."
+  just ffi-generate python {{ARGS}}
+  just ffi-generate swift {{ARGS}}
+  just ffi-generate kotlin {{ARGS}}
+  @echo "✅ All bindings generated successfully!"
+
+# Build debug version and generate Python bindings quickly (for development)
+ffi-dev-python:
+  #!/usr/bin/env bash
+  set -euo pipefail
+  
+  # Generate Python bindings first
+  just ffi-generate python --debug
+  
+  # Copy library to Python bindings directory
+  LIB_EXT=$(just _ffi-lib-ext)
+  echo "📦 Copying library to Python bindings directory..."
+  cp target/debug/libcdk_ffi.$LIB_EXT target/bindings/python/
+  
+  # Launch Python REPL with CDK FFI loaded
+  cd target/bindings/python
+  echo "🐍 Launching Python REPL with CDK FFI library loaded..."
+  echo "💡 The 'cdk_ffi' module is pre-imported and ready to use!"
+  python3 -i -c "from cdk_ffi import *; print('✅ CDK FFI library loaded successfully!');"
+
+# Test language bindings with a simple import
+ffi-test-bindings LANGUAGE: (ffi-generate LANGUAGE "--debug")
+  #!/usr/bin/env bash
+  set -euo pipefail
+  LANG="{{LANGUAGE}}"
+  LIB_EXT=$(just _ffi-lib-ext)
+  
+  echo "📦 Copying library to $LANG bindings directory..."
+  cp target/debug/libcdk_ffi.$LIB_EXT target/bindings/$LANG/
+  
+  cd target/bindings/$LANG
+  echo "🧪 Testing $LANG bindings..."
+  
+  case "$LANG" in
+    python)
+      python3 -c "import cdk_ffi; print('✅ Python bindings work!')"
+      ;;
+    *)
+      echo "✅ $LANG bindings generated (manual testing required)"
+      ;;
+  esac
+
+# Test Python bindings (shorthand)
+ffi-test-python:
+  just ffi-test-bindings python
+
+# Trigger Swift Package release workflow
+ffi-release-swift VERSION:
+  #!/usr/bin/env bash
+  set -euo pipefail
+  
+  echo "🚀 Triggering Publish Swift Package workflow..."
+  echo "   Version: {{VERSION}}"
+  echo "   CDK Ref: v{{VERSION}}"
+  
+  # Trigger the workflow using GitHub CLI
+  gh workflow run "Publish Swift Package" \
+    --repo cashubtc/cdk-swift \
+    --field version="{{VERSION}}" \
+    --field cdk_repo="cashubtc/cdk" \
+    --field cdk_ref="v{{VERSION}}"
+  
+  echo "✅ Workflow triggered successfully!"