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