Skip to main content

cdk_ffi/
wallet.rs

1//! FFI Wallet bindings
2
3use std::str::FromStr;
4use std::sync::Arc;
5
6use bip39::Mnemonic;
7use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
8
9use crate::error::FfiError;
10use crate::token::Token;
11#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
12use crate::types::bip321::BitcoinNetwork;
13use crate::types::payment_request::PaymentRequest;
14use crate::types::*;
15
16/// FFI-compatible Wallet
17
18#[derive(uniffi::Object)]
19pub struct Wallet {
20    inner: Arc<CdkWallet>,
21}
22
23impl Wallet {
24    /// Create a Wallet from an existing CDK wallet (internal use only)
25    pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
26        Self { inner }
27    }
28
29    /// Access the inner CDK wallet
30    pub(crate) fn inner(&self) -> &Arc<CdkWallet> {
31        &self.inner
32    }
33}
34
35#[uniffi::export(async_runtime = "tokio")]
36impl Wallet {
37    /// Create a new Wallet
38    ///
39    /// Accepts a `WalletStore` which can be:
40    /// - `Sqlite { path }` — built-in Rust SQLite backend
41    /// - `Postgres { url }` — built-in Rust Postgres backend
42    /// - `Custom { db }` — foreign-language implementation of `WalletDatabase`
43    #[uniffi::constructor]
44    pub fn new(
45        mint_url: String,
46        unit: CurrencyUnit,
47        mnemonic: String,
48        store: crate::database::WalletStore,
49        config: WalletConfig,
50    ) -> Result<Self, FfiError> {
51        let db = crate::database::resolve_wallet_store(store)?;
52        let localstore = crate::database::create_cdk_database_from_ffi(db);
53
54        let m = Mnemonic::parse(&mnemonic)
55            .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
56        let seed = m.to_seed_normalized("");
57
58        let wallet = CdkWalletBuilder::new()
59            .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
60                FfiError::internal(format!("Invalid URL: {}", e))
61            })?)
62            .unit(unit.into())
63            .localstore(localstore)
64            .seed(seed)
65            .target_proof_count(config.target_proof_count.unwrap_or(3) as usize)
66            .build()
67            .map_err(FfiError::from)?;
68
69        Ok(Self {
70            inner: Arc::new(wallet),
71        })
72    }
73
74    /// Get the mint URL
75    pub fn mint_url(&self) -> MintUrl {
76        self.inner.mint_url.clone().into()
77    }
78
79    /// Get the currency unit
80    pub fn unit(&self) -> CurrencyUnit {
81        self.inner.unit.clone().into()
82    }
83
84    /// Set metadata cache TTL (time-to-live) in seconds
85    ///
86    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
87    /// before requiring a refresh from the mint server.
88    ///
89    /// # Arguments
90    ///
91    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used.
92    ///
93    /// # Example
94    ///
95    /// ```ignore
96    /// // Cache expires after 5 minutes
97    /// wallet.set_metadata_cache_ttl(Some(300));
98    ///
99    /// // Cache never expires (default)
100    /// wallet.set_metadata_cache_ttl(None);
101    /// ```
102    pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
103        let ttl = ttl_secs.map(std::time::Duration::from_secs);
104        self.inner.set_metadata_cache_ttl(ttl);
105    }
106
107    /// Get total balance
108    pub async fn total_balance(&self) -> Result<Amount, FfiError> {
109        let balance = self.inner.total_balance().await?;
110        Ok(balance.into())
111    }
112
113    /// Get total pending balance
114    pub async fn total_pending_balance(&self) -> Result<Amount, FfiError> {
115        let balance = self.inner.total_pending_balance().await?;
116        Ok(balance.into())
117    }
118
119    /// Get total reserved balance
120    pub async fn total_reserved_balance(&self) -> Result<Amount, FfiError> {
121        let balance = self.inner.total_reserved_balance().await?;
122        Ok(balance.into())
123    }
124
125    /// Get mint info from mint
126    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
127        let info = self.inner.fetch_mint_info().await?;
128        Ok(info.map(Into::into))
129    }
130
131    /// Load mint info
132    ///
133    /// This will get mint info from cache if it is fresh
134    pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
135        let info = self.inner.load_mint_info().await?;
136        Ok(info.into())
137    }
138
139    /// Receive tokens
140    pub async fn receive(
141        &self,
142        token: std::sync::Arc<Token>,
143        options: ReceiveOptions,
144    ) -> Result<Amount, FfiError> {
145        let amount = self
146            .inner
147            .receive(&token.to_string(), options.try_into()?)
148            .await?;
149        Ok(amount.into())
150    }
151
152    /// Restore wallet from seed
153    pub async fn restore(&self) -> Result<Restored, FfiError> {
154        let restored = self.inner.restore().await?;
155        Ok(restored.into())
156    }
157
158    /// Restore wallet from seed with custom NUT-13 options
159    pub async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, FfiError> {
160        let restored = self.inner.restore_with_opts(opts.try_into()?).await?;
161        Ok(restored.into())
162    }
163
164    /// Verify token DLEQ proofs
165    pub async fn verify_token_dleq(&self, token: std::sync::Arc<Token>) -> Result<(), FfiError> {
166        let cdk_token = token.inner.clone();
167        self.inner.verify_token_dleq(&cdk_token).await?;
168        Ok(())
169    }
170
171    /// Receive proofs directly
172    pub async fn receive_proofs(
173        &self,
174        proofs: Proofs,
175        options: ReceiveOptions,
176        memo: Option<String>,
177        token: Option<String>,
178    ) -> Result<Amount, FfiError> {
179        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
180            proofs.into_iter().map(|p| p.try_into()).collect();
181        let cdk_proofs = cdk_proofs?;
182
183        let amount = self
184            .inner
185            .receive_proofs(cdk_proofs, options.try_into()?, memo, token)
186            .await?;
187        Ok(amount.into())
188    }
189
190    /// Get all pending send operations
191    pub async fn get_pending_sends(&self) -> Result<Vec<String>, FfiError> {
192        let sends = self.inner.get_pending_sends().await?;
193        Ok(sends.into_iter().map(|id| id.to_string()).collect())
194    }
195
196    /// Revoke a pending send operation
197    pub async fn revoke_send(&self, operation_id: String) -> Result<Amount, FfiError> {
198        let uuid = uuid::Uuid::parse_str(&operation_id)
199            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
200        let amount = self.inner.revoke_send(uuid).await?;
201        Ok(amount.into())
202    }
203
204    /// Check status of a pending send operation
205    pub async fn check_send_status(&self, operation_id: String) -> Result<bool, FfiError> {
206        let uuid = uuid::Uuid::parse_str(&operation_id)
207            .map_err(|e| FfiError::internal(format!("Invalid operation ID: {}", e)))?;
208        let claimed = self.inner.check_send_status(uuid).await?;
209        Ok(claimed)
210    }
211
212    /// Prepare a send operation
213    pub async fn prepare_send(
214        &self,
215        amount: Amount,
216        options: SendOptions,
217    ) -> Result<std::sync::Arc<PreparedSend>, FfiError> {
218        let prepared = self
219            .inner
220            .prepare_send(amount.into(), options.try_into()?)
221            .await?;
222        Ok(std::sync::Arc::new(PreparedSend::new(
223            self.inner.clone(),
224            &prepared,
225        )))
226    }
227
228    /// Get a mint quote
229    pub async fn mint_quote(
230        &self,
231        payment_method: PaymentMethod,
232        amount: Option<Amount>,
233        description: Option<String>,
234        extra: Option<String>,
235    ) -> Result<MintQuote, FfiError> {
236        let quote = self
237            .inner
238            .mint_quote(
239                payment_method.into(),
240                amount.map(Into::into),
241                description,
242                extra,
243            )
244            .await?;
245        Ok(quote.into())
246    }
247
248    /// Check a mint quote status from the mint.
249    ///
250    /// Calls `GET /v1/mint/quote/{method}/{quote_id}` per NUT-04.
251    /// Updates local store with current state from mint.
252    /// If there was a crashed mid-mint (pending saga), attempts to complete it.
253    /// Does NOT mint tokens directly - use mint() for that.
254    ///
255    /// **Note:** The mint quote must be known to the wallet (stored locally) for this
256    /// function to work. If the quote is not stored locally, use `fetch_mint_quote`
257    /// instead.
258    pub async fn check_mint_quote(&self, quote_id: String) -> Result<MintQuote, FfiError> {
259        self.check_mint_quote_status(quote_id).await
260    }
261
262    /// Check a mint quote status from the mint.
263    ///
264    /// Calls `GET /v1/mint/quote/{method}/{quote_id}` per NUT-04.
265    /// Updates local store with current state from mint.
266    /// If there was a crashed mid-mint (pending saga), attempts to complete it.
267    /// Does NOT mint tokens directly - use mint() for that.
268    ///
269    /// **Note:** The mint quote must be known to the wallet (stored locally) for this
270    /// function to work. If the quote is not stored locally, use `fetch_mint_quote`
271    /// instead.
272    pub async fn check_mint_quote_status(&self, quote_id: String) -> Result<MintQuote, FfiError> {
273        let quote = self.inner.check_mint_quote_status(&quote_id).await?;
274        Ok(quote.into())
275    }
276
277    /// Fetch a mint quote from the mint and store it locally
278    ///
279    /// Works with all payment methods (Bolt11, Bolt12, and custom payment methods).
280    ///
281    /// # Arguments
282    /// * `quote_id` - The ID of the quote to fetch
283    /// * `payment_method` - The payment method for the quote. Required if the quote
284    ///   is not already stored locally. If the quote exists locally, the stored
285    ///   payment method will be used and this parameter is ignored.
286    pub async fn fetch_mint_quote(
287        &self,
288        quote_id: String,
289        payment_method: Option<PaymentMethod>,
290    ) -> Result<MintQuote, FfiError> {
291        let method = payment_method.map(Into::into);
292        let quote = self.inner.fetch_mint_quote(&quote_id, method).await?;
293        Ok(quote.into())
294    }
295
296    /// Mint tokens
297    pub async fn mint(
298        &self,
299        quote_id: String,
300        amount_split_target: SplitTarget,
301        spending_conditions: Option<SpendingConditions>,
302    ) -> Result<Proofs, FfiError> {
303        // Convert spending conditions if provided
304        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
305
306        let proofs = self
307            .inner
308            .mint(&quote_id, amount_split_target.into(), conditions)
309            .await?;
310        Ok(proofs.into_iter().map(|p| p.into()).collect())
311    }
312
313    /// Prepare a melt operation
314    ///
315    /// Returns a `PreparedMelt` that can be confirmed or cancelled.
316    pub async fn prepare_melt(&self, quote_id: String) -> Result<PreparedMelt, FfiError> {
317        let prepared = self
318            .inner
319            .prepare_melt(&quote_id, std::collections::HashMap::new())
320            .await?;
321        Ok(PreparedMelt::new(Arc::clone(&self.inner), &prepared))
322    }
323
324    /// Prepare a melt operation with specific proofs
325    ///
326    /// This method allows melting proofs that may not be in the wallet's database,
327    /// similar to how `receive_proofs` handles external proofs. The proofs will be
328    /// added to the database and used for the melt operation.
329    ///
330    /// # Arguments
331    ///
332    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
333    /// * `proofs` - The proofs to melt (can be external proofs not in the wallet's database)
334    ///
335    /// # Returns
336    ///
337    /// A `PreparedMelt` that can be confirmed or cancelled
338    pub async fn prepare_melt_proofs(
339        &self,
340        quote_id: String,
341        proofs: Proofs,
342    ) -> Result<PreparedMelt, FfiError> {
343        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
344            proofs.into_iter().map(|p| p.try_into()).collect();
345        let cdk_proofs = cdk_proofs?;
346
347        let prepared = self
348            .inner
349            .prepare_melt_proofs(&quote_id, cdk_proofs, std::collections::HashMap::new())
350            .await?;
351        Ok(PreparedMelt::new(Arc::clone(&self.inner), &prepared))
352    }
353
354    /// Prepare a melt operation from an encoded token
355    ///
356    /// Decodes the token internally (handling keyset state for v2 keysets),
357    /// extracts proofs, and prepares the melt operation.
358    ///
359    /// # Arguments
360    ///
361    /// * `quote_id` - The melt quote ID (obtained from `melt_quote`)
362    /// * `encoded_token` - The encoded token string (cashuA or cashuB format)
363    ///
364    /// # Returns
365    ///
366    /// A `PreparedMelt` that can be confirmed or cancelled
367    pub async fn prepare_melt_token(
368        &self,
369        quote_id: String,
370        encoded_token: String,
371    ) -> Result<PreparedMelt, FfiError> {
372        let prepared = self
373            .inner
374            .prepare_melt_token(&quote_id, &encoded_token, std::collections::HashMap::new())
375            .await?;
376        Ok(PreparedMelt::new(Arc::clone(&self.inner), &prepared))
377    }
378
379    pub async fn mint_unified(
380        &self,
381        quote_id: String,
382        amount_split_target: SplitTarget,
383        spending_conditions: Option<SpendingConditions>,
384    ) -> Result<Proofs, FfiError> {
385        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
386
387        let proofs = self
388            .inner
389            .mint(&quote_id, amount_split_target.into(), conditions)
390            .await?;
391
392        Ok(proofs.into_iter().map(|p| p.into()).collect())
393    }
394    /// Get a melt quote using a unified interface for any payment method
395    ///
396    /// This method supports bolt11, bolt12, and custom payment methods.
397    /// For custom methods, you can pass extra JSON data that will be forwarded
398    /// to the payment processor.
399    ///
400    /// # Arguments
401    /// * `method` - Payment method to use (bolt11, bolt12, or custom)
402    /// * `request` - Payment request string (invoice, offer, or custom format)
403    /// * `options` - Optional melt options (MPP, amountless, etc.)
404    /// * `extra` - Optional JSON string with extra payment-method-specific fields (for custom methods)
405    pub async fn melt_quote(
406        &self,
407        method: PaymentMethod,
408        request: String,
409        options: Option<MeltOptions>,
410        extra: Option<String>,
411    ) -> Result<MeltQuote, FfiError> {
412        let cdk_options = options.map(Into::into);
413        let quote = self
414            .inner
415            .melt_quote::<cdk::nuts::PaymentMethod, _>(method.into(), request, cdk_options, extra)
416            .await?;
417        Ok(quote.into())
418    }
419
420    /// Fetch available onchain melt quote options.
421    ///
422    /// Each returned quote represents one selectable confirmation target/fee reserve.
423    /// Pass the chosen quote to `select_onchain_melt_quote`, then prepare and confirm
424    /// the returned quote ID through the normal melt flow.
425    pub async fn quote_onchain_melt_options(
426        &self,
427        address: String,
428        amount: Amount,
429        max_fee_amount: Option<Amount>,
430    ) -> Result<Vec<MeltQuote>, FfiError> {
431        let quotes = self
432            .inner
433            .quote_onchain_melt_options(&address, amount.into(), max_fee_amount.map(Into::into))
434            .await?;
435
436        Ok(quotes.into_iter().map(Into::into).collect())
437    }
438
439    /// Persist the selected onchain melt quote before preparing it.
440    pub async fn select_onchain_melt_quote(&self, quote: MeltQuote) -> Result<MeltQuote, FfiError> {
441        let quote = self
442            .inner
443            .select_onchain_melt_quote(quote.try_into()?)
444            .await?;
445        Ok(quote.into())
446    }
447
448    /// Check melt quote status and attempt to complete any in-progress saga.
449    pub async fn check_melt_quote_status(&self, quote_id: String) -> Result<MeltQuote, FfiError> {
450        let quote = self.inner.check_melt_quote_status(&quote_id).await?;
451        Ok(quote.into())
452    }
453
454    /// Finalize pending melt operations for this wallet.
455    pub async fn finalize_pending_melts(&self) -> Result<Vec<FinalizedMelt>, FfiError> {
456        let finalized = self.inner.finalize_pending_melts().await?;
457        Ok(finalized.into_iter().map(Into::into).collect())
458    }
459
460    /// Swap proofs
461    pub async fn swap(
462        &self,
463        amount: Option<Amount>,
464        amount_split_target: SplitTarget,
465        input_proofs: Proofs,
466        spending_conditions: Option<SpendingConditions>,
467        include_fees: bool,
468    ) -> Result<Option<Proofs>, FfiError> {
469        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
470            input_proofs.into_iter().map(|p| p.try_into()).collect();
471        let cdk_proofs = cdk_proofs?;
472
473        // Convert spending conditions if provided
474        let conditions = spending_conditions.map(|sc| sc.try_into()).transpose()?;
475
476        let result = self
477            .inner
478            .swap(
479                amount.map(Into::into),
480                amount_split_target.into(),
481                cdk_proofs,
482                conditions,
483                include_fees,
484                false,
485            )
486            .await?;
487
488        Ok(result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect()))
489    }
490
491    /// Get proofs by states
492    pub async fn get_proofs_by_states(&self, states: Vec<ProofState>) -> Result<Proofs, FfiError> {
493        let mut all_proofs = Vec::new();
494
495        for state in states {
496            let proofs = match state {
497                ProofState::Unspent => self.inner.get_unspent_proofs().await?,
498                ProofState::Pending => self.inner.get_pending_proofs().await?,
499                ProofState::Reserved => self.inner.get_reserved_proofs().await?,
500                ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
501                ProofState::Spent => {
502                    // CDK doesn't have a method to get spent proofs directly
503                    // They are removed from the database when spent
504                    continue;
505                }
506            };
507
508            for proof in proofs {
509                all_proofs.push(proof.into());
510            }
511        }
512
513        Ok(all_proofs)
514    }
515
516    /// Check if proofs are spent
517    pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<bool>, FfiError> {
518        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
519            proofs.into_iter().map(|p| p.try_into()).collect();
520        let cdk_proofs = cdk_proofs?;
521
522        let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
523        // Convert ProofState to bool (spent = true, unspent = false)
524        let spent_bools = proof_states
525            .into_iter()
526            .map(|proof_state| {
527                matches!(
528                    proof_state.state,
529                    cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
530                )
531            })
532            .collect();
533        Ok(spent_bools)
534    }
535
536    /// List transactions
537    pub async fn list_transactions(
538        &self,
539        direction: Option<TransactionDirection>,
540    ) -> Result<Vec<Transaction>, FfiError> {
541        let cdk_direction = direction.map(Into::into);
542        let transactions = self.inner.list_transactions(cdk_direction).await?;
543        Ok(transactions.into_iter().map(Into::into).collect())
544    }
545
546    /// Get transaction by ID
547    pub async fn get_transaction(
548        &self,
549        id: TransactionId,
550    ) -> Result<Option<Transaction>, FfiError> {
551        let cdk_id = id.try_into()?;
552        let transaction = self.inner.get_transaction(cdk_id).await?;
553        Ok(transaction.map(Into::into))
554    }
555
556    /// Get proofs for a transaction by transaction ID
557    ///
558    /// This retrieves all proofs associated with a transaction by looking up
559    /// the transaction's Y values and fetching the corresponding proofs.
560    pub async fn get_proofs_for_transaction(
561        &self,
562        id: TransactionId,
563    ) -> Result<Vec<Proof>, FfiError> {
564        let cdk_id = id.try_into()?;
565        let proofs = self.inner.get_proofs_for_transaction(cdk_id).await?;
566        Ok(proofs.into_iter().map(Into::into).collect())
567    }
568
569    /// Revert a transaction
570    pub async fn revert_transaction(&self, id: TransactionId) -> Result<(), FfiError> {
571        let cdk_id = id.try_into()?;
572        self.inner.revert_transaction(cdk_id).await?;
573        Ok(())
574    }
575
576    /// Subscribe to wallet events
577    pub async fn subscribe(
578        &self,
579        params: SubscribeParams,
580    ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
581        let cdk_params: cdk::nuts::nut17::Params<Arc<String>> = params.clone().into();
582        let sub_id = cdk_params.id.to_string();
583        let active_sub = self.inner.subscribe(cdk_params).await?;
584        Ok(std::sync::Arc::new(ActiveSubscription::new(
585            active_sub, sub_id,
586        )))
587    }
588
589    /// Subscribe to mint quote state updates
590    ///
591    /// Convenience method that creates a subscription to receive notifications
592    /// when any of the given mint quotes change state (e.g., Unpaid → Paid → Issued).
593    ///
594    /// Use `recv()` on the returned `ActiveSubscription` to receive updates as
595    /// `NotificationPayload::MintQuoteUpdate`.
596    ///
597    /// All quote IDs must belong to the same payment method.
598    ///
599    /// # Arguments
600    /// * `quote_ids` - The IDs of the mint quotes to monitor
601    /// * `payment_method` - The payment method of the quotes
602    pub async fn subscribe_mint_quote_state(
603        &self,
604        quote_ids: Vec<String>,
605        payment_method: PaymentMethod,
606    ) -> Result<std::sync::Arc<ActiveSubscription>, FfiError> {
607        let cdk_method: cdk_common::PaymentMethod = payment_method.into();
608        let active_sub = self
609            .inner
610            .subscribe_mint_quote_state(quote_ids, cdk_method)
611            .await?;
612        let sub_id = uuid::Uuid::new_v4().to_string();
613        Ok(std::sync::Arc::new(ActiveSubscription::new(
614            active_sub, sub_id,
615        )))
616    }
617
618    /// Refresh keysets from the mint
619    pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
620        let keysets = self.inner.refresh_keysets().await?;
621        Ok(keysets.into_iter().map(Into::into).collect())
622    }
623
624    /// Get the active keyset for the wallet's unit
625    pub async fn get_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
626        let keyset = self.inner.get_active_keyset().await?;
627        Ok(keyset.into())
628    }
629
630    /// Get fees for a specific keyset ID
631    pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
632        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
633        Ok(self.inner.get_keyset_fees_by_id(id).await?)
634    }
635
636    /// Load keys for a specific keyset
637    pub async fn load_keyset_keys(&self, keyset_id: String) -> Result<Keys, FfiError> {
638        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
639        let keys = self.inner.load_keyset_keys(id).await?;
640        Ok(keys.into())
641    }
642
643    /// Get keysets for this wallet's unit with filter
644    pub async fn get_mint_keysets(
645        &self,
646        filter: KeysetFilter,
647    ) -> Result<Vec<KeySetInfo>, FfiError> {
648        let keysets = self.inner.get_mint_keysets(filter.into()).await?;
649        Ok(keysets.into_iter().map(Into::into).collect())
650    }
651
652    /// Load active keysets
653    pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
654        let keysets = self.inner.load_mint_keysets().await?;
655        Ok(keysets.into_iter().map(Into::into).collect())
656    }
657
658    /// Fetch active keyset with lowest fees
659    pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, FfiError> {
660        let keyset = self.inner.fetch_active_keyset().await?;
661        Ok(keyset.into())
662    }
663
664    /// Get fees and amounts for all keysets
665    pub async fn get_keyset_fees_and_amounts(
666        &self,
667    ) -> Result<std::collections::HashMap<String, FeeAndAmounts>, FfiError> {
668        let fees = self.inner.get_keyset_fees_and_amounts().await?;
669        Ok(fees
670            .into_iter()
671            .map(|(id, fa)| (id.to_string(), fa.into()))
672            .collect())
673    }
674
675    /// Get fees and amounts for a specific keyset
676    pub async fn get_keyset_fees_and_amounts_by_id(
677        &self,
678        keyset_id: String,
679    ) -> Result<FeeAndAmounts, FfiError> {
680        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
681        let fa = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
682        Ok(fa.into())
683    }
684
685    /// Get fee for count of proofs in a keyset
686    pub async fn get_keyset_count_fee(
687        &self,
688        keyset_id: String,
689        count: u64,
690    ) -> Result<Amount, FfiError> {
691        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
692        let fee = self.inner.get_keyset_count_fee(&id, count).await?;
693        Ok(fee.into())
694    }
695
696    /// Check all pending proofs and return the total amount still pending
697    ///
698    /// This function checks orphaned pending proofs (not managed by active sagas)
699    /// with the mint and marks spent proofs accordingly.
700    pub async fn check_all_pending_proofs(&self) -> Result<Amount, FfiError> {
701        let amount = self.inner.check_all_pending_proofs().await?;
702        Ok(amount.into())
703    }
704
705    /// Recover from incomplete wallet sagas after a crash
706    ///
707    /// Handles interrupted swap, send, receive, issue, and melt operations. Requires
708    /// network access to the mint for states that need external status checks.
709    pub async fn recover_incomplete_sagas(&self) -> Result<RecoveryReport, FfiError> {
710        let report = self.inner.recover_incomplete_sagas().await?;
711        Ok(report.into())
712    }
713
714    /// Calculate fee for a given number of proofs with the specified keyset
715    pub async fn calculate_fee(
716        &self,
717        proof_count: u32,
718        keyset_id: String,
719    ) -> Result<Amount, FfiError> {
720        let id = cdk::nuts::Id::from_str(&keyset_id).map_err(FfiError::internal)?;
721        let fee = self.inner.calculate_fee(proof_count as u64, id).await?;
722        Ok(fee.into())
723    }
724
725    /// Pay a NUT-18 payment request
726    ///
727    /// This method prepares and sends a payment for the given payment request.
728    /// It will use the Nostr or HTTP transport specified in the request.
729    ///
730    /// # Arguments
731    ///
732    /// * `payment_request` - The NUT-18 payment request to pay
733    /// * `custom_amount` - Optional amount to pay (required if request has no amount)
734    pub async fn pay_request(
735        &self,
736        payment_request: std::sync::Arc<PaymentRequest>,
737        custom_amount: Option<Amount>,
738    ) -> Result<(), FfiError> {
739        self.inner
740            .pay_request(
741                payment_request.inner().clone(),
742                custom_amount.map(Into::into),
743            )
744            .await?;
745        Ok(())
746    }
747}
748
749/// BIP353 methods for Wallet
750#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
751#[uniffi::export(async_runtime = "tokio")]
752impl Wallet {
753    /// Get a quote for a BIP353 melt
754    ///
755    /// This method resolves a BIP353 address (e.g., "alice@example.com") to a Bitcoin
756    /// payment instruction, requires a BOLT12 offer, and then creates a melt quote for it.
757    ///
758    /// The `network` parameter controls which on-chain address prefixes are accepted
759    /// in the resolved URI.
760    pub async fn melt_bip353_quote(
761        &self,
762        bip353_address: String,
763        amount_msat: Amount,
764        network: BitcoinNetwork,
765    ) -> Result<MeltQuote, FfiError> {
766        let cdk_amount: cdk::Amount = amount_msat.into();
767        let quote = self
768            .inner
769            .melt_bip353_quote(&bip353_address, cdk_amount, network.into())
770            .await?;
771        Ok(quote.into())
772    }
773
774    /// Get a quote for a Lightning address melt
775    ///
776    /// This method resolves a Lightning address (e.g., "alice@example.com") to a Lightning invoice
777    /// and then creates a melt quote for that invoice.
778    pub async fn melt_lightning_address_quote(
779        &self,
780        lightning_address: String,
781        amount_msat: Amount,
782    ) -> Result<MeltQuote, FfiError> {
783        let cdk_amount: cdk::Amount = amount_msat.into();
784        let quote = self
785            .inner
786            .melt_lightning_address_quote(&lightning_address, cdk_amount)
787            .await?;
788        Ok(quote.into())
789    }
790
791    /// Get a quote for a human-readable address melt
792    ///
793    /// This method accepts a human-readable address that could be either a BIP353 address
794    /// or a Lightning address. It intelligently determines which to try based on mint support:
795    ///
796    /// 1. If the mint supports Bolt12, it tries BIP353 first
797    /// 2. Falls back to Lightning address only if BIP353 resolution fails
798    /// 3. If BIP353 resolves but has no usable BOLT12 offer, it does NOT fall back
799    /// 4. If the mint doesn't support Bolt12, it tries Lightning address directly
800    ///
801    /// The `network` parameter is forwarded to the BIP353 resolver for on-chain address
802    /// validation in the resolved URI.
803    pub async fn melt_human_readable(
804        &self,
805        address: String,
806        amount_msat: Amount,
807        network: BitcoinNetwork,
808    ) -> Result<MeltQuote, FfiError> {
809        self.melt_human_readable_quote(address, amount_msat, network)
810            .await
811    }
812
813    /// Get a quote for a human-readable address melt
814    ///
815    /// Accepts a human-readable address that could be either a BIP353 address
816    /// or a Lightning address. Tries BIP353 first if mint supports Bolt12,
817    /// falls back to Lightning address.
818    pub async fn melt_human_readable_quote(
819        &self,
820        address: String,
821        amount_msat: Amount,
822        network: BitcoinNetwork,
823    ) -> Result<MeltQuote, FfiError> {
824        let cdk_amount: cdk::Amount = amount_msat.into();
825        let quote = self
826            .inner
827            .melt_human_readable_quote(&address, cdk_amount, network.into())
828            .await?;
829        Ok(quote.into())
830    }
831}
832
833/// Auth methods for Wallet
834#[uniffi::export(async_runtime = "tokio")]
835impl Wallet {
836    /// Set Clear Auth Token (CAT) for authentication
837    pub async fn set_cat(&self, cat: String) -> Result<(), FfiError> {
838        self.inner.set_cat(cat).await?;
839        Ok(())
840    }
841
842    /// Set refresh token for authentication
843    pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), FfiError> {
844        self.inner.set_refresh_token(refresh_token).await?;
845        Ok(())
846    }
847
848    /// Refresh access token using the stored refresh token
849    pub async fn refresh_access_token(&self) -> Result<(), FfiError> {
850        self.inner.refresh_access_token().await?;
851        Ok(())
852    }
853
854    /// Mint blind auth tokens
855    pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, FfiError> {
856        let proofs = self.inner.mint_blind_auth(amount.into()).await?;
857        Ok(proofs.into_iter().map(|p| p.into()).collect())
858    }
859
860    /// Get unspent auth proofs
861    pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, FfiError> {
862        let auth_proofs = self.inner.get_unspent_auth_proofs().await?;
863        Ok(auth_proofs.into_iter().map(Into::into).collect())
864    }
865}
866
867/// Configuration for creating wallets
868#[derive(Debug, Clone, uniffi::Record)]
869pub struct WalletConfig {
870    pub target_proof_count: Option<u32>,
871}
872
873/// Generates a new random mnemonic phrase
874#[uniffi::export]
875pub fn generate_mnemonic() -> Result<String, FfiError> {
876    let mnemonic = Mnemonic::generate(12)
877        .map_err(|e| FfiError::internal(format!("Failed to generate mnemonic: {}", e)))?;
878    Ok(mnemonic.to_string())
879}
880
881/// Converts a mnemonic phrase to its entropy bytes
882#[uniffi::export]
883pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, FfiError> {
884    let m = Mnemonic::parse(&mnemonic)
885        .map_err(|e| FfiError::internal(format!("Invalid mnemonic: {}", e)))?;
886    Ok(m.to_entropy())
887}