//! FFI Wallet bindings use std::str::FromStr; use std::sync::Arc; use bip39::Mnemonic; use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder}; use crate::error::FfiError; use crate::token::Token; use crate::types::payment_request::PaymentRequest; use crate::types::*; /// FFI-compatible Wallet #[derive(uniffi::Object)] pub struct Wallet { inner: Arc, } impl Wallet { /// Create a Wallet from an existing CDK wallet (internal use only) pub(crate) fn from_inner(inner: Arc) -> Self { Self { inner } } } #[uniffi::export(async_runtime = "tokio")] impl Wallet { /// Create a new Wallet from mnemonic using WalletDatabaseFfi trait #[uniffi::constructor] pub fn new( mint_url: String, unit: CurrencyUnit, mnemonic: String, db: Arc, config: WalletConfig, ) -> Result { // Parse mnemonic and generate seed without passphrase let m = Mnemonic::parse(&mnemonic) .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?; let seed = m.to_seed_normalized(""); tracing::info!("creating ffi wallet"); // Convert the FFI database trait to a CDK database implementation let localstore = crate::database::create_cdk_database_from_ffi(db); let wallet = CdkWalletBuilder::new() .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| { FfiError::internal(format!("Invalid URL: {}", e)) })?) .unit(unit.into()) .localstore(localstore) .seed(seed) .target_proof_count(config.target_proof_count.unwrap_or(3) as usize) .build() .map_err(FfiError::from)?; Ok(Self { inner: Arc::new(wallet), }) } /// Get the mint URL pub fn mint_url(&self) -> MintUrl { self.inner.mint_url.clone().into() } /// Get the currency unit pub fn unit(&self) -> CurrencyUnit { self.inner.unit.clone().into() } /// Set metadata cache TTL (time-to-live) in seconds /// /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh /// before requiring a refresh from the mint server. /// /// # Arguments /// /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used. /// /// # Example /// /// ```ignore /// // Cache expires after 5 minutes /// wallet.set_metadata_cache_ttl(Some(300)); /// /// // Cache never expires (default) /// wallet.set_metadata_cache_ttl(None); /// ``` pub fn set_metadata_cache_ttl(&self, ttl_secs: Option) { let ttl = ttl_secs.map(std::time::Duration::from_secs); self.inner.set_metadata_cache_ttl(ttl); } /// Get total balance pub async fn total_balance(&self) -> Result { let balance = self.inner.total_balance().await?; Ok(balance.into()) } /// Get total pending balance pub async fn total_pending_balance(&self) -> Result { let balance = self.inner.total_pending_balance().await?; Ok(balance.into()) } /// Get total reserved balance pub async fn total_reserved_balance(&self) -> Result { let balance = self.inner.total_reserved_balance().await?; Ok(balance.into()) } /// Get mint info from mint pub async fn fetch_mint_info(&self) -> Result, FfiError> { let info = self.inner.fetch_mint_info().await?; Ok(info.map(Into::into)) } /// Load mint info /// /// This will get mint info from cache if it is fresh pub async fn load_mint_info(&self) -> Result { let info = self.inner.load_mint_info().await?; Ok(info.into()) } /// Receive tokens pub async fn receive( &self, token: std::sync::Arc, options: ReceiveOptions, ) -> Result { let amount = self .inner .receive(&token.to_string(), options.into()) .await?; Ok(amount.into()) } /// Restore wallet from seed pub async fn restore(&self) -> Result { let restored = self.inner.restore().await?; Ok(restored.into()) } /// Verify token DLEQ proofs pub async fn verify_token_dleq(&self, token: std::sync::Arc) -> Result<(), FfiError> { let cdk_token = token.inner.clone(); self.inner.verify_token_dleq(&cdk_token).await?; Ok(()) } /// Receive proofs directly pub async fn receive_proofs( &self, proofs: Proofs, options: ReceiveOptions, memo: Option, ) -> Result { let cdk_proofs: Result, _> = proofs.into_iter().map(|p| p.try_into()).collect(); let cdk_proofs = cdk_proofs?; let amount = self .inner .receive_proofs(cdk_proofs, options.into(), memo) .await?; Ok(amount.into()) } /// Prepare a send operation pub async fn prepare_send( &self, amount: Amount, options: SendOptions, ) -> Result, FfiError> { let prepared = self .inner .prepare_send(amount.into(), options.into()) .await?; Ok(std::sync::Arc::new(prepared.into())) } /// Get a mint quote pub async fn mint_quote( &self, amount: Amount, description: Option, ) -> Result { let quote = self.inner.mint_quote(amount.into(), description).await?; Ok(quote.into()) } /// Mint tokens pub async fn mint( &self, quote_id: String, amount_split_target: SplitTarget, spending_conditions: Option, ) -> Result { // Convert spending conditions if provided let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?; let proofs = self .inner .mint("e_id, amount_split_target.into(), conditions) .await?; Ok(proofs.into_iter().map(|p| p.into()).collect()) } /// Get a melt quote pub async fn melt_quote( &self, request: String, options: Option, ) -> Result { let cdk_options = options.map(Into::into); let quote = self.inner.melt_quote(request, cdk_options).await?; Ok(quote.into()) } /// Melt tokens pub async fn melt(&self, quote_id: String) -> Result { let melted = self.inner.melt("e_id).await?; Ok(melted.into()) } /// Melt specific proofs /// /// This method allows melting proofs that may not be in the wallet's database, /// similar to how `receive_proofs` handles external proofs. The proofs will be /// added to the database and used for the melt operation. /// /// # Arguments /// /// * `quote_id` - The melt quote ID (obtained from `melt_quote`) /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database) /// /// # Returns /// /// A `Melted` result containing the payment details and any change proofs pub async fn melt_proofs(&self, quote_id: String, proofs: Proofs) -> Result { let cdk_proofs: Result, _> = proofs.into_iter().map(|p| p.try_into()).collect(); let cdk_proofs = cdk_proofs?; let melted = self.inner.melt_proofs("e_id, cdk_proofs).await?; Ok(melted.into()) } /// Get a quote for a bolt12 mint pub async fn mint_bolt12_quote( &self, amount: Option, description: Option, ) -> Result { let quote = self .inner .mint_bolt12_quote(amount.map(Into::into), description) .await?; Ok(quote.into()) } /// Get a mint quote using a unified interface for any payment method /// /// This method supports bolt11, bolt12, and custom payment methods. /// For custom methods, you can pass extra JSON data that will be forwarded /// to the payment processor. /// /// # Arguments /// * `amount` - Optional amount to mint (required for bolt11) /// * `method` - Payment method to use (bolt11, bolt12, or custom) /// * `description` - Optional description for the quote /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods) pub async fn mint_quote_unified( &self, amount: Option, method: PaymentMethod, description: Option, extra: Option, ) -> Result { let quote = self .inner .mint_quote_unified(amount.map(Into::into), method.into(), description, extra) .await?; Ok(quote.into()) } /// Mint tokens using bolt12 pub async fn mint_bolt12( &self, quote_id: String, amount: Option, amount_split_target: SplitTarget, spending_conditions: Option, ) -> Result { let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?; let proofs = self .inner .mint_bolt12( "e_id, amount.map(Into::into), amount_split_target.into(), conditions, ) .await?; Ok(proofs.into_iter().map(|p| p.into()).collect()) } pub async fn mint_unified( &self, quote_id: String, amount: Option, amount_split_target: SplitTarget, spending_conditions: Option, ) -> Result { let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?; let proofs = self .inner .mint_unified( "e_id, amount.map(Into::into), amount_split_target.into(), conditions, ) .await?; Ok(proofs.into_iter().map(|p| p.into()).collect()) } /// Get a quote for a bolt12 melt pub async fn melt_bolt12_quote( &self, request: String, options: Option, ) -> Result { let cdk_options = options.map(Into::into); let quote = self.inner.melt_bolt12_quote(request, cdk_options).await?; Ok(quote.into()) } /// Get a melt quote using a unified interface for any payment method /// /// This method supports bolt11, bolt12, and custom payment methods. /// For custom methods, you can pass extra JSON data that will be forwarded /// to the payment processor. /// /// # Arguments /// * `method` - Payment method to use (bolt11, bolt12, or custom) /// * `request` - Payment request string (invoice, offer, or custom format) /// * `options` - Optional melt options (MPP, amountless, etc.) /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods) pub async fn melt_quote_unified( &self, method: PaymentMethod, request: String, options: Option, extra: Option, ) -> Result { // Parse the extra JSON string into a serde_json::Value let extra_value = extra .map(|s| serde_json::from_str(&s)) .transpose() .map_err(|e| FfiError::internal(format!("Invalid extra JSON: {}", e)))?; let cdk_options = options.map(Into::into); let quote = self .inner .melt_quote_unified(method.into(), request, cdk_options, extra_value) .await?; Ok(quote.into()) } /// Swap proofs pub async fn swap( &self, amount: Option, amount_split_target: SplitTarget, input_proofs: Proofs, spending_conditions: Option, include_fees: bool, ) -> Result, FfiError> { let cdk_proofs: Result, _> = input_proofs.into_iter().map(|p| p.try_into()).collect(); let cdk_proofs = cdk_proofs?; // Convert spending conditions if provided let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?; let result = self .inner .swap( amount.map(Into::into), amount_split_target.into(), cdk_proofs, conditions, include_fees, ) .await?; Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect())) } /// Get proofs by states pub async fn get_proofs_by_states(&self, states: Vec) -> Result { let mut all_proofs = Vec::new(); for state in states { let proofs = match state { ProofState::Unspent => self.inner.get_unspent_proofs().await?, ProofState::Pending => self.inner.get_pending_proofs().await?, ProofState::Reserved => self.inner.get_reserved_proofs().await?, ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?, ProofState::Spent => { // CDK doesn't have a method to get spent proofs directly // They are removed from the database when spent continue; } }; for proof in proofs { all_proofs.push(proof.into()); } } Ok(all_proofs) } /// Check if proofs are spent pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, FfiError> { let cdk_proofs: Result, _> = proofs.into_iter().map(|p| p.try_into()).collect(); let cdk_proofs = cdk_proofs?; let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?; // Convert ProofState to bool (spent = true, unspent = false) let spent_bools = proof_states .into_iter() .map(|proof_state| { matches!( proof_state.state, cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent ) }) .collect(); Ok(spent_bools) } /// List transactions pub async fn list_transactions( &self, direction: Option, ) -> Result, FfiError> { let cdk_direction = direction.map(Into::into); let transactions = self.inner.list_transactions(cdk_direction).await?; Ok(transactions.into_iter().map(Into::into).collect()) } /// Get transaction by ID pub async fn get_transaction( &self, id: TransactionId, ) -> Result, FfiError> { let cdk_id = id.try_into()?; let transaction = self.inner.get_transaction(cdk_id).await?; Ok(transaction.map(Into::into)) } /// Get proofs for a transaction by transaction ID /// /// This retrieves all proofs associated with a transaction by looking up /// the transaction's Y values and fetching the corresponding proofs. pub async fn get_proofs_for_transaction( &self, id: TransactionId, ) -> Result, FfiError> { let cdk_id = id.try_into()?; let proofs = self.inner.get_proofs_for_transaction(cdk_id).await?; Ok(proofs.into_iter().map(Into::into).collect()) } /// Revert a transaction pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> { let cdk_id = id.try_into()?; self.inner.revert_transaction(cdk_id).await?; Ok(()) } /// Subscribe to wallet events pub async fn subscribe( &self, params: SubscribeParams, ) -> Result, FfiError> { let cdk_params: cdk::nuts::nut17::Params> = params.clone().into(); let sub_id = cdk_params.id.to_string(); let active_sub = self.inner.subscribe(cdk_params).await?; Ok(std::sync::Arc::new(ActiveSubscription::new( active_sub, sub_id, ))) } /// Refresh keysets from the mint pub async fn refresh_keysets(&self) -> Result, FfiError> { let keysets = self.inner.refresh_keysets().await?; Ok(keysets.into_iter().map(Into::into).collect()) } /// Get the active keyset for the wallet's unit pub async fn get_active_keyset(&self) -> Result { let keyset = self.inner.get_active_keyset().await?; Ok(keyset.into()) } /// Get fees for a specific keyset ID pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result { let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?; Ok(self .inner .get_keyset_fees_and_amounts_by_id(id) .await? .fee()) } /// Reclaim unspent proofs (mark them as unspent in the database) pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), FfiError> { let cdk_proofs: Result, _> = proofs.iter().map(|p| p.clone().try_into()).collect(); let cdk_proofs = cdk_proofs?; self.inner.reclaim_unspent(cdk_proofs).await?; Ok(()) } /// Check all pending proofs and return the total amount reclaimed pub async fn check_all_pending_proofs(&self) -> Result { let amount = self.inner.check_all_pending_proofs().await?; Ok(amount.into()) } /// Calculate fee for a given number of proofs with the specified keyset pub async fn calculate_fee( &self, proof_count: u32, keyset_id: String, ) -> Result { let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?; let fee = self .inner .get_keyset_count_fee(&id, proof_count as u64) .await?; Ok(fee.into()) } /// Pay a NUT-18 payment request /// /// This method prepares and sends a payment for the given payment request. /// It will use the Nostr or HTTP transport specified in the request. /// /// # Arguments /// /// * `payment_request` - The NUT-18 payment request to pay /// * `custom_amount` - Optional amount to pay (required if request has no amount) pub async fn pay_request( &self, payment_request: std::sync::Arc, custom_amount: Option, ) -> Result<(), FfiError> { self.inner .pay_request( payment_request.inner().clone(), custom_amount.map(Into::into), ) .await?; Ok(()) } } /// BIP353 methods for Wallet #[cfg(not(target_arch = "wasm32"))] #[uniffi::export(async_runtime = "tokio")] impl Wallet { /// Get a quote for a BIP353 melt /// /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Lightning offer /// and then creates a melt quote for that offer. pub async fn melt_bip353_quote( &self, bip353_address: String, amount_msat: Amount, ) -> Result { let cdk_amount: cdk::Amount = amount_msat.into(); let quote = self .inner .melt_bip353_quote(&bip353_address, cdk_amount) .await?; Ok(quote.into()) } /// Get a quote for a Lightning address melt /// /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice /// and then creates a melt quote for that invoice. pub async fn melt_lightning_address_quote( &self, lightning_address: String, amount_msat: Amount, ) -> Result { let cdk_amount: cdk::Amount = amount_msat.into(); let quote = self .inner .melt_lightning_address_quote(&lightning_address, cdk_amount) .await?; Ok(quote.into()) } /// Get a quote for a human-readable address melt /// /// This method accepts a human-readable address that could be either a BIP353 address /// or a Lightning address. It intelligently determines which to try based on mint support: /// /// 1. If the mint supports Bolt12, it tries BIP353 first /// 2. Falls back to Lightning address only if BIP353 DNS resolution fails /// 3. If BIP353 resolves but fails at the mint, it does NOT fall back to Lightning address /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly pub async fn melt_human_readable( &self, address: String, amount_msat: Amount, ) -> Result { let cdk_amount: cdk::Amount = amount_msat.into(); let quote = self .inner .melt_human_readable_quote(&address, cdk_amount) .await?; Ok(quote.into()) } } /// Auth methods for Wallet #[uniffi::export(async_runtime = "tokio")] impl Wallet { /// Set Clear Auth Token (CAT) for authentication pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> { self.inner.set_cat(cat).await?; Ok(()) } /// Set refresh token for authentication pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> { self.inner.set_refresh_token(refresh_token).await?; Ok(()) } /// Refresh access token using the stored refresh token pub async fn refresh_access_token(&self) -> Result<(), FfiError> { self.inner.refresh_access_token().await?; Ok(()) } /// Mint blind auth tokens pub async fn mint_blind_auth(&self, amount: Amount) -> Result { let proofs = self.inner.mint_blind_auth(amount.into()).await?; Ok(proofs.into_iter().map(|p| p.into()).collect()) } /// Get unspent auth proofs pub async fn get_unspent_auth_proofs(&self) -> Result, FfiError> { let auth_proofs = self.inner.get_unspent_auth_proofs().await?; Ok(auth_proofs.into_iter().map(Into::into).collect()) } } /// Configuration for creating wallets #[derive(Debug, Clone, uniffi::Record)] pub struct WalletConfig { pub target_proof_count: Option, } /// Generates a new random mnemonic phrase #[uniffi::export] pub fn generate_mnemonic() -> Result { let mnemonic = Mnemonic::generate(12) .map_err(|e| FfiError::internal(format!("Failed to generate mnemonic: {}", e)))?; Ok(mnemonic.to_string()) } /// Converts a mnemonic phrase to its entropy bytes #[uniffi::export] pub fn mnemonic_to_entropy(mnemonic: String) -> Result, FfiError> { let m = Mnemonic::parse(&mnemonic) .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?; Ok(m.to_entropy()) }