Sfoglia il codice sorgente

Split uniffi types into multiple mods (#1142)

David Caseria 1 mese fa
parent
commit
a8c35dbef0

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

@@ -1,2883 +0,0 @@
-//! 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::pub_sub::SubId;
-use cdk::Amount as CdkAmount;
-use serde::{Deserialize, Serialize};
-
-use crate::error::FfiError;
-use crate::token::Token;
-
-/// 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 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::wallet::SendKind {
-    fn from(kind: SendKind) -> Self {
-        match kind {
-            SendKind::OnlineExact => cdk::wallet::SendKind::OnlineExact,
-            SendKind::OnlineTolerance { tolerance } => {
-                cdk::wallet::SendKind::OnlineTolerance(tolerance.into())
-            }
-            SendKind::OfflineExact => cdk::wallet::SendKind::OfflineExact,
-            SendKind::OfflineTolerance { tolerance } => {
-                cdk::wallet::SendKind::OfflineTolerance(tolerance.into())
-            }
-        }
-    }
-}
-
-impl From<cdk::wallet::SendKind> for SendKind {
-    fn from(kind: cdk::wallet::SendKind) -> Self {
-        match kind {
-            cdk::wallet::SendKind::OnlineExact => SendKind::OnlineExact,
-            cdk::wallet::SendKind::OnlineTolerance(tolerance) => SendKind::OnlineTolerance {
-                tolerance: tolerance.into(),
-            },
-            cdk::wallet::SendKind::OfflineExact => SendKind::OfflineExact,
-            cdk::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::types::Melted> for Melted {
-    fn from(melted: cdk::types::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,
-        }
-    }
-}
-
-// -----------------------------
-// NUT-04/05 FFI Types
-// -----------------------------
-
-/// FFI-compatible MintMethodSettings (NUT-04)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct MintMethodSettings {
-    pub method: PaymentMethod,
-    pub unit: CurrencyUnit,
-    pub min_amount: Option<Amount>,
-    pub max_amount: Option<Amount>,
-    /// For bolt11, whether mint supports setting invoice description
-    pub description: Option<bool>,
-}
-
-impl From<cdk::nuts::nut04::MintMethodSettings> for MintMethodSettings {
-    fn from(s: cdk::nuts::nut04::MintMethodSettings) -> Self {
-        let description = match s.options {
-            Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) => Some(description),
-            _ => None,
-        };
-        Self {
-            method: s.method.into(),
-            unit: s.unit.into(),
-            min_amount: s.min_amount.map(Into::into),
-            max_amount: s.max_amount.map(Into::into),
-            description,
-        }
-    }
-}
-
-impl TryFrom<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
-    type Error = FfiError;
-
-    fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
-        let options = match (s.method.clone(), s.description) {
-            (PaymentMethod::Bolt11, Some(description)) => {
-                Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description })
-            }
-            _ => None,
-        };
-        Ok(Self {
-            method: s.method.into(),
-            unit: s.unit.into(),
-            min_amount: s.min_amount.map(Into::into),
-            max_amount: s.max_amount.map(Into::into),
-            options,
-        })
-    }
-}
-
-/// FFI-compatible Nut04 Settings
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct Nut04Settings {
-    pub methods: Vec<MintMethodSettings>,
-    pub disabled: bool,
-}
-
-impl From<cdk::nuts::nut04::Settings> for Nut04Settings {
-    fn from(s: cdk::nuts::nut04::Settings) -> Self {
-        Self {
-            methods: s.methods.into_iter().map(Into::into).collect(),
-            disabled: s.disabled,
-        }
-    }
-}
-
-impl TryFrom<Nut04Settings> for cdk::nuts::nut04::Settings {
-    type Error = FfiError;
-
-    fn try_from(s: Nut04Settings) -> Result<Self, Self::Error> {
-        Ok(Self {
-            methods: s
-                .methods
-                .into_iter()
-                .map(TryInto::try_into)
-                .collect::<Result<_, _>>()?,
-            disabled: s.disabled,
-        })
-    }
-}
-
-/// FFI-compatible MeltMethodSettings (NUT-05)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct MeltMethodSettings {
-    pub method: PaymentMethod,
-    pub unit: CurrencyUnit,
-    pub min_amount: Option<Amount>,
-    pub max_amount: Option<Amount>,
-    /// For bolt11, whether mint supports amountless invoices
-    pub amountless: Option<bool>,
-}
-
-impl From<cdk::nuts::nut05::MeltMethodSettings> for MeltMethodSettings {
-    fn from(s: cdk::nuts::nut05::MeltMethodSettings) -> Self {
-        let amountless = match s.options {
-            Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) => Some(amountless),
-            _ => None,
-        };
-        Self {
-            method: s.method.into(),
-            unit: s.unit.into(),
-            min_amount: s.min_amount.map(Into::into),
-            max_amount: s.max_amount.map(Into::into),
-            amountless,
-        }
-    }
-}
-
-impl TryFrom<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
-    type Error = FfiError;
-
-    fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
-        let options = match (s.method.clone(), s.amountless) {
-            (PaymentMethod::Bolt11, Some(amountless)) => {
-                Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless })
-            }
-            _ => None,
-        };
-        Ok(Self {
-            method: s.method.into(),
-            unit: s.unit.into(),
-            min_amount: s.min_amount.map(Into::into),
-            max_amount: s.max_amount.map(Into::into),
-            options,
-        })
-    }
-}
-
-/// FFI-compatible Nut05 Settings
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct Nut05Settings {
-    pub methods: Vec<MeltMethodSettings>,
-    pub disabled: bool,
-}
-
-impl From<cdk::nuts::nut05::Settings> for Nut05Settings {
-    fn from(s: cdk::nuts::nut05::Settings) -> Self {
-        Self {
-            methods: s.methods.into_iter().map(Into::into).collect(),
-            disabled: s.disabled,
-        }
-    }
-}
-
-impl TryFrom<Nut05Settings> for cdk::nuts::nut05::Settings {
-    type Error = FfiError;
-
-    fn try_from(s: Nut05Settings) -> Result<Self, Self::Error> {
-        Ok(Self {
-            methods: s
-                .methods
-                .into_iter()
-                .map(TryInto::try_into)
-                .collect::<Result<_, _>>()?,
-            disabled: s.disabled,
-        })
-    }
-}
-
-/// FFI-compatible ProtectedEndpoint (for auth nuts)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct ProtectedEndpoint {
-    /// HTTP method (GET, POST, etc.)
-    pub method: String,
-    /// Endpoint path
-    pub path: String,
-}
-
-/// FFI-compatible ClearAuthSettings (NUT-21)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct ClearAuthSettings {
-    /// OpenID Connect discovery URL
-    pub openid_discovery: String,
-    /// OAuth 2.0 client ID
-    pub client_id: String,
-    /// Protected endpoints requiring clear authentication
-    pub protected_endpoints: Vec<ProtectedEndpoint>,
-}
-
-/// FFI-compatible BlindAuthSettings (NUT-22)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct BlindAuthSettings {
-    /// Maximum number of blind auth tokens that can be minted per request
-    pub bat_max_mint: u64,
-    /// Protected endpoints requiring blind authentication
-    pub protected_endpoints: Vec<ProtectedEndpoint>,
-}
-
-impl From<cdk::nuts::ClearAuthSettings> for ClearAuthSettings {
-    fn from(settings: cdk::nuts::ClearAuthSettings) -> Self {
-        Self {
-            openid_discovery: settings.openid_discovery,
-            client_id: settings.client_id,
-            protected_endpoints: settings
-                .protected_endpoints
-                .into_iter()
-                .map(Into::into)
-                .collect(),
-        }
-    }
-}
-
-impl TryFrom<ClearAuthSettings> for cdk::nuts::ClearAuthSettings {
-    type Error = FfiError;
-
-    fn try_from(settings: ClearAuthSettings) -> Result<Self, Self::Error> {
-        Ok(Self {
-            openid_discovery: settings.openid_discovery,
-            client_id: settings.client_id,
-            protected_endpoints: settings
-                .protected_endpoints
-                .into_iter()
-                .map(|e| e.try_into())
-                .collect::<Result<Vec<_>, _>>()?,
-        })
-    }
-}
-
-impl From<cdk::nuts::BlindAuthSettings> for BlindAuthSettings {
-    fn from(settings: cdk::nuts::BlindAuthSettings) -> Self {
-        Self {
-            bat_max_mint: settings.bat_max_mint,
-            protected_endpoints: settings
-                .protected_endpoints
-                .into_iter()
-                .map(Into::into)
-                .collect(),
-        }
-    }
-}
-
-impl TryFrom<BlindAuthSettings> for cdk::nuts::BlindAuthSettings {
-    type Error = FfiError;
-
-    fn try_from(settings: BlindAuthSettings) -> Result<Self, Self::Error> {
-        Ok(Self {
-            bat_max_mint: settings.bat_max_mint,
-            protected_endpoints: settings
-                .protected_endpoints
-                .into_iter()
-                .map(|e| e.try_into())
-                .collect::<Result<Vec<_>, _>>()?,
-        })
-    }
-}
-
-impl From<cdk::nuts::ProtectedEndpoint> for ProtectedEndpoint {
-    fn from(endpoint: cdk::nuts::ProtectedEndpoint) -> Self {
-        Self {
-            method: match endpoint.method {
-                cdk::nuts::Method::Get => "GET".to_string(),
-                cdk::nuts::Method::Post => "POST".to_string(),
-            },
-            path: endpoint.path.to_string(),
-        }
-    }
-}
-
-impl TryFrom<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
-    type Error = FfiError;
-
-    fn try_from(endpoint: ProtectedEndpoint) -> Result<Self, Self::Error> {
-        let method = match endpoint.method.as_str() {
-            "GET" => cdk::nuts::Method::Get,
-            "POST" => cdk::nuts::Method::Post,
-            _ => {
-                return Err(FfiError::Generic {
-                    msg: format!(
-                        "Invalid HTTP method: {}. Only GET and POST are supported",
-                        endpoint.method
-                    ),
-                })
-            }
-        };
-
-        // Convert path string to RoutePath by matching against known paths
-        let route_path = match endpoint.path.as_str() {
-            "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuoteBolt11,
-            "/v1/mint/bolt11" => cdk::nuts::RoutePath::MintBolt11,
-            "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuoteBolt11,
-            "/v1/melt/bolt11" => cdk::nuts::RoutePath::MeltBolt11,
-            "/v1/swap" => cdk::nuts::RoutePath::Swap,
-            "/v1/checkstate" => cdk::nuts::RoutePath::Checkstate,
-            "/v1/restore" => cdk::nuts::RoutePath::Restore,
-            "/v1/auth/blind/mint" => cdk::nuts::RoutePath::MintBlindAuth,
-            "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuoteBolt12,
-            "/v1/mint/bolt12" => cdk::nuts::RoutePath::MintBolt12,
-            "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuoteBolt12,
-            "/v1/melt/bolt12" => cdk::nuts::RoutePath::MeltBolt12,
-            _ => {
-                return Err(FfiError::Generic {
-                    msg: format!("Unknown route path: {}", endpoint.path),
-                })
-            }
-        };
-
-        Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path))
-    }
-}
-
-/// FFI-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings)
-#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
-pub struct Nuts {
-    /// NUT04 Settings
-    pub nut04: Nut04Settings,
-    /// NUT05 Settings
-    pub nut05: Nut05Settings,
-    /// NUT07 Settings - Token state check
-    pub nut07_supported: bool,
-    /// NUT08 Settings - Lightning fee return
-    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,
-    /// NUT21 Settings - Clear authentication
-    pub nut21: Option<ClearAuthSettings>,
-    /// NUT22 Settings - Blind authentication
-    pub nut22: Option<BlindAuthSettings>,
-    /// Supported currency units for minting
-    pub mint_units: Vec<CurrencyUnit>,
-    /// Supported currency units for melting
-    pub melt_units: Vec<CurrencyUnit>,
-}
-
-impl From<cdk::nuts::Nuts> for Nuts {
-    fn from(nuts: cdk::nuts::Nuts) -> Self {
-        let mint_units = nuts
-            .supported_mint_units()
-            .into_iter()
-            .map(|u| u.clone().into())
-            .collect();
-        let melt_units = nuts
-            .supported_melt_units()
-            .into_iter()
-            .map(|u| u.clone().into())
-            .collect();
-
-        Self {
-            nut04: nuts.nut04.clone().into(),
-            nut05: nuts.nut05.clone().into(),
-            nut07_supported: nuts.nut07.supported,
-            nut08_supported: nuts.nut08.supported,
-            nut09_supported: nuts.nut09.supported,
-            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,
-            nut21: nuts.nut21.map(Into::into),
-            nut22: nuts.nut22.map(Into::into),
-            mint_units,
-            melt_units,
-        }
-    }
-}
-
-impl TryFrom<Nuts> for cdk::nuts::Nuts {
-    type Error = FfiError;
-
-    fn try_from(n: Nuts) -> Result<Self, Self::Error> {
-        Ok(Self {
-            nut04: n.nut04.try_into()?,
-            nut05: n.nut05.try_into()?,
-            nut07: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut07_supported,
-            },
-            nut08: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut08_supported,
-            },
-            nut09: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut09_supported,
-            },
-            nut10: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut10_supported,
-            },
-            nut11: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut11_supported,
-            },
-            nut12: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut12_supported,
-            },
-            nut14: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut14_supported,
-            },
-            nut15: Default::default(),
-            nut17: Default::default(),
-            nut19: Default::default(),
-            nut20: cdk::nuts::nut06::SupportedSettings {
-                supported: n.nut20_supported,
-            },
-            nut21: n.nut21.map(|s| s.try_into()).transpose()?,
-            nut22: n.nut22.map(|s| s.try_into()).transpose()?,
-        })
-    }
-}
-
-impl Nuts {
-    /// Convert Nuts to JSON string
-    pub fn to_json(&self) -> Result<String, FfiError> {
-        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::nuts::MintInfo {
-    fn from(info: MintInfo) -> Self {
-        // Convert FFI Nuts back to cdk::nuts::Nuts (best-effort)
-        let nuts_cdk: cdk::nuts::Nuts = info.nuts.clone().try_into().unwrap_or_default();
-        Self {
-            name: info.name,
-            pubkey: info.pubkey.and_then(|p| p.parse().ok()),
-            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: nuts_cdk,
-            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::wallet::types::Transaction> for Transaction {
-    fn from(tx: cdk::wallet::types::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::wallet::types::Transaction {
-    type Error = FfiError;
-
-    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
-        let cdk_ys: Result<Vec<cdk::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::wallet::types::TransactionDirection> for TransactionDirection {
-    fn from(direction: cdk::wallet::types::TransactionDirection) -> Self {
-        match direction {
-            cdk::wallet::types::TransactionDirection::Incoming => TransactionDirection::Incoming,
-            cdk::wallet::types::TransactionDirection::Outgoing => TransactionDirection::Outgoing,
-        }
-    }
-}
-
-impl From<TransactionDirection> for cdk::wallet::types::TransactionDirection {
-    fn from(direction: TransactionDirection) -> Self {
-        match direction {
-            TransactionDirection::Incoming => cdk::wallet::types::TransactionDirection::Incoming,
-            TransactionDirection::Outgoing => cdk::wallet::types::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::wallet::types::TransactionId::from_proofs(cdk_proofs)?;
-        Ok(Self {
-            hex: id.to_string(),
-        })
-    }
-}
-
-impl From<cdk::wallet::types::TransactionId> for TransactionId {
-    fn from(id: cdk::wallet::types::TransactionId) -> Self {
-        Self {
-            hex: id.to_string(),
-        }
-    }
-}
-
-impl TryFrom<TransactionId> for cdk::wallet::types::TransactionId {
-    type Error = FfiError;
-
-    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
-        cdk::wallet::types::TransactionId::from_hex(&id.hex)
-            .map_err(|e| FfiError::InvalidHex { msg: e.to_string() })
-    }
-}
-
-/// FFI-compatible AuthProof
-#[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,
-}
-
-impl From<cdk::nuts::AuthProof> for AuthProof {
-    fn from(auth_proof: cdk::nuts::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()),
-        }
-    }
-}
-
-impl TryFrom<AuthProof> for cdk::nuts::AuthProof {
-    type Error = FfiError;
-
-    fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
-        use std::str::FromStr;
-        Ok(Self {
-            keyset_id: cdk::nuts::Id::from_str(&auth_proof.keyset_id)
-                .map_err(|e| FfiError::Serialization { msg: e.to_string() })?,
-            secret: {
-                use std::str::FromStr;
-                cdk::secret::Secret::from_str(&auth_proof.secret)
-                    .map_err(|e| FfiError::Serialization { msg: e.to_string() })?
-            },
-            c: cdk::nuts::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
-        })
-    }
-}
-
-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
-#[uniffi::export]
-pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
-    Ok(serde_json::from_str(&json)?)
-}
-
-/// Encode AuthProof to JSON string
-#[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::nuts::nut17::Params<cdk::pub_sub::SubId> {
-    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::nuts::KeySetInfo> for KeySetInfo {
-    fn from(keyset: cdk::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::nuts::KeySetInfo {
-    fn from(keyset: KeySetInfo) -> Self {
-        use std::str::FromStr;
-        Self {
-            id: cdk::nuts::Id::from_str(&keyset.id).unwrap(),
-            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::nuts::PublicKey> for PublicKey {
-    fn from(key: cdk::nuts::PublicKey) -> Self {
-        Self {
-            hex: key.to_string(),
-        }
-    }
-}
-
-impl TryFrom<PublicKey> for cdk::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::nuts::Keys> for Keys {
-    fn from(keys: cdk::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::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 = cdk::Amount::from(amount_u64);
-            let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex)
-                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
-            keys_map.insert(amount, pubkey);
-        }
-
-        Ok(cdk::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<cdk::nuts::KeySet> for KeySet {
-    fn from(keyset: cdk::nuts::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 cdk::nuts::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 = cdk::nuts::Id::from_str(&keyset.id)
-            .map_err(|e| FfiError::Serialization { msg: e.to_string() })?;
-
-        // Convert unit
-        let unit: cdk::nuts::CurrencyUnit = keyset.unit.into();
-
-        // Convert keys
-        let mut keys_map = BTreeMap::new();
-        for (amount_u64, pubkey_hex) in keyset.keys {
-            let amount = cdk::Amount::from(amount_u64);
-            let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex)
-                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
-            keys_map.insert(amount, pubkey);
-        }
-        let keys = cdk::nuts::Keys::new(keys_map);
-
-        Ok(cdk::nuts::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::types::ProofInfo> for ProofInfo {
-    fn from(info: cdk::types::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::types::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::types::ProofInfo for serialization
-    let cdk_info = cdk::types::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::nuts::Id> for Id {
-    fn from(id: cdk::nuts::Id) -> Self {
-        Self {
-            hex: id.to_string(),
-        }
-    }
-}
-
-impl From<Id> for cdk::nuts::Id {
-    fn from(id: Id) -> Self {
-        use std::str::FromStr;
-        Self::from_str(&id.hex).unwrap()
-    }
-}

+ 166 - 0
crates/cdk-ffi/src/types/amount.rs

@@ -0,0 +1,166 @@
+//! Amount and currency related types
+
+use cdk::nuts::CurrencyUnit as CdkCurrencyUnit;
+use cdk::Amount as CdkAmount;
+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 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(),
+            },
+        }
+    }
+}

+ 258 - 0
crates/cdk-ffi/src/types/keys.rs

@@ -0,0 +1,258 @@
+//! Key-related FFI types
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::CurrencyUnit;
+use crate::error::FfiError;
+
+/// 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::nuts::KeySetInfo> for KeySetInfo {
+    fn from(keyset: cdk::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::nuts::KeySetInfo {
+    fn from(keyset: KeySetInfo) -> Self {
+        use std::str::FromStr;
+        Self {
+            id: cdk::nuts::Id::from_str(&keyset.id).unwrap(),
+            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::nuts::PublicKey> for PublicKey {
+    fn from(key: cdk::nuts::PublicKey) -> Self {
+        Self {
+            hex: key.to_string(),
+        }
+    }
+}
+
+impl TryFrom<PublicKey> for cdk::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::nuts::Keys> for Keys {
+    fn from(keys: cdk::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::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 = cdk::Amount::from(amount_u64);
+            let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex)
+                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
+            keys_map.insert(amount, pubkey);
+        }
+
+        Ok(cdk::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<cdk::nuts::KeySet> for KeySet {
+    fn from(keyset: cdk::nuts::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 cdk::nuts::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 = cdk::nuts::Id::from_str(&keyset.id)
+            .map_err(|e| FfiError::Serialization { msg: e.to_string() })?;
+
+        // Convert unit
+        let unit: cdk::nuts::CurrencyUnit = keyset.unit.into();
+
+        // Convert keys
+        let mut keys_map = BTreeMap::new();
+        for (amount_u64, pubkey_hex) in keyset.keys {
+            let amount = cdk::Amount::from(amount_u64);
+            let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex)
+                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?;
+            keys_map.insert(amount, pubkey);
+        }
+        let keys = cdk::nuts::Keys::new(keys_map);
+
+        Ok(cdk::nuts::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 Id (for keyset IDs)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+#[serde(transparent)]
+pub struct Id {
+    pub hex: String,
+}
+
+impl From<cdk::nuts::Id> for Id {
+    fn from(id: cdk::nuts::Id) -> Self {
+        Self {
+            hex: id.to_string(),
+        }
+    }
+}
+
+impl From<Id> for cdk::nuts::Id {
+    fn from(id: Id) -> Self {
+        use std::str::FromStr;
+        Self::from_str(&id.hex).unwrap()
+    }
+}

+ 675 - 0
crates/cdk-ffi/src/types/mint.rs

@@ -0,0 +1,675 @@
+//! Mint-related FFI types
+
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::quote::PaymentMethod;
+use crate::error::FfiError;
+
+/// 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 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,
+        }
+    }
+}
+
+// -----------------------------
+// NUT-04/05 FFI Types
+// -----------------------------
+
+/// FFI-compatible MintMethodSettings (NUT-04)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MintMethodSettings {
+    pub method: PaymentMethod,
+    pub unit: CurrencyUnit,
+    pub min_amount: Option<Amount>,
+    pub max_amount: Option<Amount>,
+    /// For bolt11, whether mint supports setting invoice description
+    pub description: Option<bool>,
+}
+
+impl From<cdk::nuts::nut04::MintMethodSettings> for MintMethodSettings {
+    fn from(s: cdk::nuts::nut04::MintMethodSettings) -> Self {
+        let description = match s.options {
+            Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) => Some(description),
+            _ => None,
+        };
+        Self {
+            method: s.method.into(),
+            unit: s.unit.into(),
+            min_amount: s.min_amount.map(Into::into),
+            max_amount: s.max_amount.map(Into::into),
+            description,
+        }
+    }
+}
+
+impl TryFrom<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
+    type Error = FfiError;
+
+    fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
+        let options = match (s.method.clone(), s.description) {
+            (PaymentMethod::Bolt11, Some(description)) => {
+                Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description })
+            }
+            _ => None,
+        };
+        Ok(Self {
+            method: s.method.into(),
+            unit: s.unit.into(),
+            min_amount: s.min_amount.map(Into::into),
+            max_amount: s.max_amount.map(Into::into),
+            options,
+        })
+    }
+}
+
+/// FFI-compatible Nut04 Settings
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Nut04Settings {
+    pub methods: Vec<MintMethodSettings>,
+    pub disabled: bool,
+}
+
+impl From<cdk::nuts::nut04::Settings> for Nut04Settings {
+    fn from(s: cdk::nuts::nut04::Settings) -> Self {
+        Self {
+            methods: s.methods.into_iter().map(Into::into).collect(),
+            disabled: s.disabled,
+        }
+    }
+}
+
+impl TryFrom<Nut04Settings> for cdk::nuts::nut04::Settings {
+    type Error = FfiError;
+
+    fn try_from(s: Nut04Settings) -> Result<Self, Self::Error> {
+        Ok(Self {
+            methods: s
+                .methods
+                .into_iter()
+                .map(TryInto::try_into)
+                .collect::<Result<_, _>>()?,
+            disabled: s.disabled,
+        })
+    }
+}
+
+/// FFI-compatible MeltMethodSettings (NUT-05)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct MeltMethodSettings {
+    pub method: PaymentMethod,
+    pub unit: CurrencyUnit,
+    pub min_amount: Option<Amount>,
+    pub max_amount: Option<Amount>,
+    /// For bolt11, whether mint supports amountless invoices
+    pub amountless: Option<bool>,
+}
+
+impl From<cdk::nuts::nut05::MeltMethodSettings> for MeltMethodSettings {
+    fn from(s: cdk::nuts::nut05::MeltMethodSettings) -> Self {
+        let amountless = match s.options {
+            Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) => Some(amountless),
+            _ => None,
+        };
+        Self {
+            method: s.method.into(),
+            unit: s.unit.into(),
+            min_amount: s.min_amount.map(Into::into),
+            max_amount: s.max_amount.map(Into::into),
+            amountless,
+        }
+    }
+}
+
+impl TryFrom<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
+    type Error = FfiError;
+
+    fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
+        let options = match (s.method.clone(), s.amountless) {
+            (PaymentMethod::Bolt11, Some(amountless)) => {
+                Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless })
+            }
+            _ => None,
+        };
+        Ok(Self {
+            method: s.method.into(),
+            unit: s.unit.into(),
+            min_amount: s.min_amount.map(Into::into),
+            max_amount: s.max_amount.map(Into::into),
+            options,
+        })
+    }
+}
+
+/// FFI-compatible Nut05 Settings
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Nut05Settings {
+    pub methods: Vec<MeltMethodSettings>,
+    pub disabled: bool,
+}
+
+impl From<cdk::nuts::nut05::Settings> for Nut05Settings {
+    fn from(s: cdk::nuts::nut05::Settings) -> Self {
+        Self {
+            methods: s.methods.into_iter().map(Into::into).collect(),
+            disabled: s.disabled,
+        }
+    }
+}
+
+impl TryFrom<Nut05Settings> for cdk::nuts::nut05::Settings {
+    type Error = FfiError;
+
+    fn try_from(s: Nut05Settings) -> Result<Self, Self::Error> {
+        Ok(Self {
+            methods: s
+                .methods
+                .into_iter()
+                .map(TryInto::try_into)
+                .collect::<Result<_, _>>()?,
+            disabled: s.disabled,
+        })
+    }
+}
+
+/// FFI-compatible ProtectedEndpoint (for auth nuts)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ProtectedEndpoint {
+    /// HTTP method (GET, POST, etc.)
+    pub method: String,
+    /// Endpoint path
+    pub path: String,
+}
+
+/// FFI-compatible ClearAuthSettings (NUT-21)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct ClearAuthSettings {
+    /// OpenID Connect discovery URL
+    pub openid_discovery: String,
+    /// OAuth 2.0 client ID
+    pub client_id: String,
+    /// Protected endpoints requiring clear authentication
+    pub protected_endpoints: Vec<ProtectedEndpoint>,
+}
+
+/// FFI-compatible BlindAuthSettings (NUT-22)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct BlindAuthSettings {
+    /// Maximum number of blind auth tokens that can be minted per request
+    pub bat_max_mint: u64,
+    /// Protected endpoints requiring blind authentication
+    pub protected_endpoints: Vec<ProtectedEndpoint>,
+}
+
+impl From<cdk::nuts::ClearAuthSettings> for ClearAuthSettings {
+    fn from(settings: cdk::nuts::ClearAuthSettings) -> Self {
+        Self {
+            openid_discovery: settings.openid_discovery,
+            client_id: settings.client_id,
+            protected_endpoints: settings
+                .protected_endpoints
+                .into_iter()
+                .map(Into::into)
+                .collect(),
+        }
+    }
+}
+
+impl TryFrom<ClearAuthSettings> for cdk::nuts::ClearAuthSettings {
+    type Error = FfiError;
+
+    fn try_from(settings: ClearAuthSettings) -> Result<Self, Self::Error> {
+        Ok(Self {
+            openid_discovery: settings.openid_discovery,
+            client_id: settings.client_id,
+            protected_endpoints: settings
+                .protected_endpoints
+                .into_iter()
+                .map(|e| e.try_into())
+                .collect::<Result<Vec<_>, _>>()?,
+        })
+    }
+}
+
+impl From<cdk::nuts::BlindAuthSettings> for BlindAuthSettings {
+    fn from(settings: cdk::nuts::BlindAuthSettings) -> Self {
+        Self {
+            bat_max_mint: settings.bat_max_mint,
+            protected_endpoints: settings
+                .protected_endpoints
+                .into_iter()
+                .map(Into::into)
+                .collect(),
+        }
+    }
+}
+
+impl TryFrom<BlindAuthSettings> for cdk::nuts::BlindAuthSettings {
+    type Error = FfiError;
+
+    fn try_from(settings: BlindAuthSettings) -> Result<Self, Self::Error> {
+        Ok(Self {
+            bat_max_mint: settings.bat_max_mint,
+            protected_endpoints: settings
+                .protected_endpoints
+                .into_iter()
+                .map(|e| e.try_into())
+                .collect::<Result<Vec<_>, _>>()?,
+        })
+    }
+}
+
+impl From<cdk::nuts::ProtectedEndpoint> for ProtectedEndpoint {
+    fn from(endpoint: cdk::nuts::ProtectedEndpoint) -> Self {
+        Self {
+            method: match endpoint.method {
+                cdk::nuts::Method::Get => "GET".to_string(),
+                cdk::nuts::Method::Post => "POST".to_string(),
+            },
+            path: endpoint.path.to_string(),
+        }
+    }
+}
+
+impl TryFrom<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
+    type Error = FfiError;
+
+    fn try_from(endpoint: ProtectedEndpoint) -> Result<Self, Self::Error> {
+        let method = match endpoint.method.as_str() {
+            "GET" => cdk::nuts::Method::Get,
+            "POST" => cdk::nuts::Method::Post,
+            _ => {
+                return Err(FfiError::Generic {
+                    msg: format!(
+                        "Invalid HTTP method: {}. Only GET and POST are supported",
+                        endpoint.method
+                    ),
+                })
+            }
+        };
+
+        // Convert path string to RoutePath by matching against known paths
+        let route_path = match endpoint.path.as_str() {
+            "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuoteBolt11,
+            "/v1/mint/bolt11" => cdk::nuts::RoutePath::MintBolt11,
+            "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuoteBolt11,
+            "/v1/melt/bolt11" => cdk::nuts::RoutePath::MeltBolt11,
+            "/v1/swap" => cdk::nuts::RoutePath::Swap,
+            "/v1/checkstate" => cdk::nuts::RoutePath::Checkstate,
+            "/v1/restore" => cdk::nuts::RoutePath::Restore,
+            "/v1/auth/blind/mint" => cdk::nuts::RoutePath::MintBlindAuth,
+            "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuoteBolt12,
+            "/v1/mint/bolt12" => cdk::nuts::RoutePath::MintBolt12,
+            "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuoteBolt12,
+            "/v1/melt/bolt12" => cdk::nuts::RoutePath::MeltBolt12,
+            _ => {
+                return Err(FfiError::Generic {
+                    msg: format!("Unknown route path: {}", endpoint.path),
+                })
+            }
+        };
+
+        Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path))
+    }
+}
+
+/// FFI-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings)
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Nuts {
+    /// NUT04 Settings
+    pub nut04: Nut04Settings,
+    /// NUT05 Settings
+    pub nut05: Nut05Settings,
+    /// NUT07 Settings - Token state check
+    pub nut07_supported: bool,
+    /// NUT08 Settings - Lightning fee return
+    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,
+    /// NUT21 Settings - Clear authentication
+    pub nut21: Option<ClearAuthSettings>,
+    /// NUT22 Settings - Blind authentication
+    pub nut22: Option<BlindAuthSettings>,
+    /// Supported currency units for minting
+    pub mint_units: Vec<CurrencyUnit>,
+    /// Supported currency units for melting
+    pub melt_units: Vec<CurrencyUnit>,
+}
+
+impl From<cdk::nuts::Nuts> for Nuts {
+    fn from(nuts: cdk::nuts::Nuts) -> Self {
+        let mint_units = nuts
+            .supported_mint_units()
+            .into_iter()
+            .map(|u| u.clone().into())
+            .collect();
+        let melt_units = nuts
+            .supported_melt_units()
+            .into_iter()
+            .map(|u| u.clone().into())
+            .collect();
+
+        Self {
+            nut04: nuts.nut04.clone().into(),
+            nut05: nuts.nut05.clone().into(),
+            nut07_supported: nuts.nut07.supported,
+            nut08_supported: nuts.nut08.supported,
+            nut09_supported: nuts.nut09.supported,
+            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,
+            nut21: nuts.nut21.map(Into::into),
+            nut22: nuts.nut22.map(Into::into),
+            mint_units,
+            melt_units,
+        }
+    }
+}
+
+impl TryFrom<Nuts> for cdk::nuts::Nuts {
+    type Error = FfiError;
+
+    fn try_from(n: Nuts) -> Result<Self, Self::Error> {
+        Ok(Self {
+            nut04: n.nut04.try_into()?,
+            nut05: n.nut05.try_into()?,
+            nut07: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut07_supported,
+            },
+            nut08: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut08_supported,
+            },
+            nut09: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut09_supported,
+            },
+            nut10: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut10_supported,
+            },
+            nut11: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut11_supported,
+            },
+            nut12: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut12_supported,
+            },
+            nut14: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut14_supported,
+            },
+            nut15: Default::default(),
+            nut17: Default::default(),
+            nut19: Default::default(),
+            nut20: cdk::nuts::nut06::SupportedSettings {
+                supported: n.nut20_supported,
+            },
+            nut21: n.nut21.map(|s| s.try_into()).transpose()?,
+            nut22: n.nut22.map(|s| s.try_into()).transpose()?,
+        })
+    }
+}
+
+impl Nuts {
+    /// Convert Nuts to JSON string
+    pub fn to_json(&self) -> Result<String, FfiError> {
+        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::nuts::MintInfo {
+    fn from(info: MintInfo) -> Self {
+        // Convert FFI Nuts back to cdk::nuts::Nuts (best-effort)
+        let nuts_cdk: cdk::nuts::Nuts = info.nuts.clone().try_into().unwrap_or_default();
+        Self {
+            name: info.name,
+            pubkey: info.pubkey.and_then(|p| p.parse().ok()),
+            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: nuts_cdk,
+            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)?)
+}

+ 24 - 0
crates/cdk-ffi/src/types/mod.rs

@@ -0,0 +1,24 @@
+//! FFI-compatible types
+//!
+//! This module contains all the FFI types used by the UniFFI bindings.
+//! Types are organized into logical submodules for better maintainability.
+
+// Module declarations
+pub mod amount;
+pub mod keys;
+pub mod mint;
+pub mod proof;
+pub mod quote;
+pub mod subscription;
+pub mod transaction;
+pub mod wallet;
+
+// Re-export all types for convenient access
+pub use amount::*;
+pub use keys::*;
+pub use mint::*;
+pub use proof::*;
+pub use quote::*;
+pub use subscription::*;
+pub use transaction::*;
+pub use wallet::*;

+ 509 - 0
crates/cdk-ffi/src/types/proof.rs

@@ -0,0 +1,509 @@
+//! Proof-related FFI types
+
+use std::str::FromStr;
+
+use cdk::nuts::State as CdkState;
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::mint::MintUrl;
+use crate::error::FfiError;
+
+/// 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 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 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),
+            },
+        }
+    }
+}
+
+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 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: super::keys::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::types::ProofInfo> for ProofInfo {
+    fn from(info: cdk::types::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::types::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::types::ProofInfo for serialization
+    let cdk_info = cdk::types::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)?)
+}
+
+/// 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)?)
+}

+ 430 - 0
crates/cdk-ffi/src/types/quote.rs

@@ -0,0 +1,430 @@
+//! Quote-related FFI types
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::mint::MintUrl;
+use crate::error::FfiError;
+
+/// 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

+ 183 - 0
crates/cdk-ffi/src/types/subscription.rs

@@ -0,0 +1,183 @@
+//! Subscription-related FFI types
+
+use cdk::pub_sub::SubId;
+use serde::{Deserialize, Serialize};
+
+use super::proof::ProofStateUpdate;
+use super::quote::{MeltQuoteBolt11Response, MintQuoteBolt11Response};
+use crate::error::FfiError;
+
+/// 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::nuts::nut17::Params<cdk::pub_sub::SubId> {
+    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![],
+                }
+            }
+        }
+    }
+}

+ 247 - 0
crates/cdk-ffi/src/types/transaction.rs

@@ -0,0 +1,247 @@
+//! Transaction-related FFI types
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::keys::PublicKey;
+use super::mint::MintUrl;
+use super::proof::Proofs;
+use crate::error::FfiError;
+
+/// 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::wallet::types::Transaction> for Transaction {
+    fn from(tx: cdk::wallet::types::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::wallet::types::Transaction {
+    type Error = FfiError;
+
+    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
+        let cdk_ys: Result<Vec<cdk::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::wallet::types::TransactionDirection> for TransactionDirection {
+    fn from(direction: cdk::wallet::types::TransactionDirection) -> Self {
+        match direction {
+            cdk::wallet::types::TransactionDirection::Incoming => TransactionDirection::Incoming,
+            cdk::wallet::types::TransactionDirection::Outgoing => TransactionDirection::Outgoing,
+        }
+    }
+}
+
+impl From<TransactionDirection> for cdk::wallet::types::TransactionDirection {
+    fn from(direction: TransactionDirection) -> Self {
+        match direction {
+            TransactionDirection::Incoming => cdk::wallet::types::TransactionDirection::Incoming,
+            TransactionDirection::Outgoing => cdk::wallet::types::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::wallet::types::TransactionId::from_proofs(cdk_proofs)?;
+        Ok(Self {
+            hex: id.to_string(),
+        })
+    }
+}
+
+impl From<cdk::wallet::types::TransactionId> for TransactionId {
+    fn from(id: cdk::wallet::types::TransactionId) -> Self {
+        Self {
+            hex: id.to_string(),
+        }
+    }
+}
+
+impl TryFrom<TransactionId> for cdk::wallet::types::TransactionId {
+    type Error = FfiError;
+
+    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
+        cdk::wallet::types::TransactionId::from_hex(&id.hex)
+            .map_err(|e| FfiError::InvalidHex { msg: e.to_string() })
+    }
+}
+
+/// FFI-compatible AuthProof
+#[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,
+}
+
+impl From<cdk::nuts::AuthProof> for AuthProof {
+    fn from(auth_proof: cdk::nuts::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()),
+        }
+    }
+}
+
+impl TryFrom<AuthProof> for cdk::nuts::AuthProof {
+    type Error = FfiError;
+
+    fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
+        use std::str::FromStr;
+        Ok(Self {
+            keyset_id: cdk::nuts::Id::from_str(&auth_proof.keyset_id)
+                .map_err(|e| FfiError::Serialization { msg: e.to_string() })?,
+            secret: {
+                use std::str::FromStr;
+                cdk::secret::Secret::from_str(&auth_proof.secret)
+                    .map_err(|e| FfiError::Serialization { msg: e.to_string() })?
+            },
+            c: cdk::nuts::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
+        })
+    }
+}
+
+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
+#[uniffi::export]
+pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode AuthProof to JSON string
+#[uniffi::export]
+pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&proof)?)
+}

+ 471 - 0
crates/cdk-ffi/src/types/wallet.rs

@@ -0,0 +1,471 @@
+//! Wallet-related FFI types
+
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, SplitTarget};
+use super::proof::{Proofs, SpendingConditions};
+use crate::error::FfiError;
+use crate::token::Token;
+
+/// 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 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::wallet::SendKind {
+    fn from(kind: SendKind) -> Self {
+        match kind {
+            SendKind::OnlineExact => cdk::wallet::SendKind::OnlineExact,
+            SendKind::OnlineTolerance { tolerance } => {
+                cdk::wallet::SendKind::OnlineTolerance(tolerance.into())
+            }
+            SendKind::OfflineExact => cdk::wallet::SendKind::OfflineExact,
+            SendKind::OfflineTolerance { tolerance } => {
+                cdk::wallet::SendKind::OfflineTolerance(tolerance.into())
+            }
+        }
+    }
+}
+
+impl From<cdk::wallet::SendKind> for SendKind {
+    fn from(kind: cdk::wallet::SendKind) -> Self {
+        match kind {
+            cdk::wallet::SendKind::OnlineExact => SendKind::OnlineExact,
+            cdk::wallet::SendKind::OnlineTolerance(tolerance) => SendKind::OnlineTolerance {
+                tolerance: tolerance.into(),
+            },
+            cdk::wallet::SendKind::OfflineExact => SendKind::OfflineExact,
+            cdk::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 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: super::quote::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::types::Melted> for Melted {
+    fn from(melted: cdk::types::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(),
+            },
+        }
+    }
+}