Skip to main content

cdk_ffi/types/
wallet.rs

1//! Wallet-related FFI types
2
3use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6
7use cdk_common::bitcoin;
8use serde::{Deserialize, Serialize};
9
10use super::amount::{Amount, SplitTarget};
11use super::proof::{Proofs, SpendingConditions};
12use crate::error::FfiError;
13use crate::token::Token;
14use crate::{CurrencyUnit, MintUrl, PublicKey};
15
16/// FFI-compatible SendMemo
17#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
18pub struct SendMemo {
19    /// Memo text
20    pub memo: String,
21    /// Include memo in token
22    pub include_memo: bool,
23}
24
25impl From<SendMemo> for cdk::wallet::SendMemo {
26    fn from(memo: SendMemo) -> Self {
27        cdk::wallet::SendMemo {
28            memo: memo.memo,
29            include_memo: memo.include_memo,
30        }
31    }
32}
33
34impl From<cdk::wallet::SendMemo> for SendMemo {
35    fn from(memo: cdk::wallet::SendMemo) -> Self {
36        Self {
37            memo: memo.memo,
38            include_memo: memo.include_memo,
39        }
40    }
41}
42
43impl SendMemo {
44    /// Convert SendMemo to JSON string
45    pub fn to_json(&self) -> Result<String, FfiError> {
46        Ok(serde_json::to_string(self)?)
47    }
48}
49
50/// Decode SendMemo from JSON string
51#[uniffi::export]
52pub fn decode_send_memo(json: String) -> Result<SendMemo, FfiError> {
53    Ok(serde_json::from_str(&json)?)
54}
55
56/// Encode SendMemo to JSON string
57#[uniffi::export]
58pub fn encode_send_memo(memo: SendMemo) -> Result<String, FfiError> {
59    Ok(serde_json::to_string(&memo)?)
60}
61
62/// FFI-compatible SendKind
63#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
64pub enum SendKind {
65    /// Allow online swap before send if wallet does not have exact amount
66    OnlineExact,
67    /// Prefer offline send if difference is less than tolerance
68    OnlineTolerance { tolerance: Amount },
69    /// Wallet cannot do an online swap and selected proof must be exactly send amount
70    OfflineExact,
71    /// Wallet must remain offline but can over pay if below tolerance
72    OfflineTolerance { tolerance: Amount },
73}
74
75impl From<SendKind> for cdk::wallet::SendKind {
76    fn from(kind: SendKind) -> Self {
77        match kind {
78            SendKind::OnlineExact => cdk::wallet::SendKind::OnlineExact,
79            SendKind::OnlineTolerance { tolerance } => {
80                cdk::wallet::SendKind::OnlineTolerance(tolerance.into())
81            }
82            SendKind::OfflineExact => cdk::wallet::SendKind::OfflineExact,
83            SendKind::OfflineTolerance { tolerance } => {
84                cdk::wallet::SendKind::OfflineTolerance(tolerance.into())
85            }
86        }
87    }
88}
89
90/// FFI-compatible P2PKSigningKey
91#[derive(Debug, Clone, uniffi::Record)]
92pub struct P2PKSigningKey {
93    /// Public key
94    pub pubkey: PublicKey,
95    /// Derivation path as string
96    pub derivation_path: String,
97    /// Derivation index
98    pub derivation_index: u32,
99    /// Created time
100    pub created_time: u64,
101}
102
103impl TryFrom<P2PKSigningKey> for cdk_common::wallet::P2PKSigningKey {
104    type Error = crate::error::FfiError;
105
106    fn try_from(key: P2PKSigningKey) -> Result<Self, FfiError> {
107        Ok(Self {
108            pubkey: key.pubkey.try_into()?,
109            derivation_path: key
110                .derivation_path
111                .parse()
112                .map_err(|e: bitcoin::bip32::Error| FfiError::Internal {
113                    error_message: e.to_string(),
114                })?,
115            derivation_index: key.derivation_index,
116            created_time: key.created_time,
117        })
118    }
119}
120
121impl From<cdk_common::wallet::P2PKSigningKey> for P2PKSigningKey {
122    fn from(key: cdk_common::wallet::P2PKSigningKey) -> Self {
123        Self {
124            pubkey: key.pubkey.into(),
125            derivation_path: key.derivation_path.to_string(),
126            derivation_index: key.derivation_index,
127            created_time: key.created_time,
128        }
129    }
130}
131
132impl From<cdk::wallet::SendKind> for SendKind {
133    fn from(kind: cdk::wallet::SendKind) -> Self {
134        match kind {
135            cdk::wallet::SendKind::OnlineExact => SendKind::OnlineExact,
136            cdk::wallet::SendKind::OnlineTolerance(tolerance) => SendKind::OnlineTolerance {
137                tolerance: tolerance.into(),
138            },
139            cdk::wallet::SendKind::OfflineExact => SendKind::OfflineExact,
140            cdk::wallet::SendKind::OfflineTolerance(tolerance) => SendKind::OfflineTolerance {
141                tolerance: tolerance.into(),
142            },
143        }
144    }
145}
146
147/// FFI-compatible P2PK locked proof send mode
148#[derive(
149    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum, Default,
150)]
151pub enum P2PKLockedProofSendMode {
152    /// Swap locked proofs into fresh proofs before creating the token
153    #[default]
154    Swap,
155    /// Sign locked proofs and include them directly in the token
156    SignAndSend,
157}
158
159impl From<P2PKLockedProofSendMode> for cdk::wallet::P2PKLockedProofSendMode {
160    fn from(mode: P2PKLockedProofSendMode) -> Self {
161        match mode {
162            P2PKLockedProofSendMode::Swap => cdk::wallet::P2PKLockedProofSendMode::Swap,
163            P2PKLockedProofSendMode::SignAndSend => {
164                cdk::wallet::P2PKLockedProofSendMode::SignAndSend
165            }
166        }
167    }
168}
169
170impl From<cdk::wallet::P2PKLockedProofSendMode> for P2PKLockedProofSendMode {
171    fn from(mode: cdk::wallet::P2PKLockedProofSendMode) -> Self {
172        match mode {
173            cdk::wallet::P2PKLockedProofSendMode::Swap => P2PKLockedProofSendMode::Swap,
174            cdk::wallet::P2PKLockedProofSendMode::SignAndSend => {
175                P2PKLockedProofSendMode::SignAndSend
176            }
177        }
178    }
179}
180
181/// FFI-compatible Send options
182#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
183pub struct SendOptions {
184    /// Memo
185    pub memo: Option<SendMemo>,
186    /// Spending conditions
187    pub conditions: Option<SpendingConditions>,
188    /// Amount split target
189    pub amount_split_target: SplitTarget,
190    /// Send kind
191    pub send_kind: SendKind,
192    /// Include fee
193    pub include_fee: bool,
194    pub use_p2bk: bool,
195    /// Maximum number of proofs to include in the token
196    pub max_proofs: Option<u32>,
197    /// Metadata
198    pub metadata: HashMap<String, String>,
199    /// Signing keys for P2PK-locked input proofs
200    #[serde(default)]
201    pub p2pk_signing_keys: Vec<SecretKey>,
202    /// How P2PK-locked input proofs should be handled during send
203    #[serde(default)]
204    pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
205}
206
207impl Default for SendOptions {
208    fn default() -> Self {
209        Self {
210            memo: None,
211            conditions: None,
212            amount_split_target: SplitTarget::None,
213            send_kind: SendKind::OnlineExact,
214            include_fee: false,
215            max_proofs: None,
216            metadata: HashMap::new(),
217            use_p2bk: false,
218            p2pk_signing_keys: Vec::new(),
219            p2pk_locked_proof_send_mode: P2PKLockedProofSendMode::Swap,
220        }
221    }
222}
223
224impl TryFrom<SendOptions> for cdk::wallet::SendOptions {
225    type Error = FfiError;
226
227    fn try_from(opts: SendOptions) -> Result<Self, Self::Error> {
228        let p2pk_signing_keys = opts
229            .p2pk_signing_keys
230            .into_iter()
231            .map(TryInto::try_into)
232            .collect::<Result<Vec<_>, _>>()?;
233
234        Ok(cdk::wallet::SendOptions {
235            memo: opts.memo.map(Into::into),
236            conditions: opts.conditions.map(TryInto::try_into).transpose()?,
237            amount_split_target: opts.amount_split_target.into(),
238            send_kind: opts.send_kind.into(),
239            include_fee: opts.include_fee,
240            max_proofs: opts.max_proofs.map(|p| p as usize),
241            metadata: opts.metadata,
242            use_p2bk: opts.use_p2bk,
243            p2pk_signing_keys,
244            p2pk_locked_proof_send_mode: opts.p2pk_locked_proof_send_mode.into(),
245        })
246    }
247}
248
249impl From<cdk::wallet::SendOptions> for SendOptions {
250    fn from(opts: cdk::wallet::SendOptions) -> Self {
251        Self {
252            memo: opts.memo.map(Into::into),
253            conditions: opts.conditions.map(Into::into),
254            amount_split_target: opts.amount_split_target.into(),
255            send_kind: opts.send_kind.into(),
256            include_fee: opts.include_fee,
257            max_proofs: opts.max_proofs.map(|p| p as u32),
258            metadata: opts.metadata,
259            use_p2bk: opts.use_p2bk,
260            p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(),
261            p2pk_locked_proof_send_mode: opts.p2pk_locked_proof_send_mode.into(),
262        }
263    }
264}
265
266impl SendOptions {
267    /// Convert SendOptions to JSON string
268    pub fn to_json(&self) -> Result<String, FfiError> {
269        Ok(serde_json::to_string(self)?)
270    }
271}
272
273/// Decode SendOptions from JSON string
274#[uniffi::export]
275pub fn decode_send_options(json: String) -> Result<SendOptions, FfiError> {
276    Ok(serde_json::from_str(&json)?)
277}
278
279/// Encode SendOptions to JSON string
280#[uniffi::export]
281pub fn encode_send_options(options: SendOptions) -> Result<String, FfiError> {
282    Ok(serde_json::to_string(&options)?)
283}
284
285/// FFI-compatible SecretKey
286#[derive(Clone, Serialize, Deserialize, uniffi::Record)]
287#[serde(transparent)]
288pub struct SecretKey {
289    /// Hex-encoded secret key (64 characters)
290    pub hex: String,
291}
292
293impl fmt::Debug for SecretKey {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.debug_struct("SecretKey")
296            .field("hex", &"[redacted]")
297            .finish()
298    }
299}
300
301impl SecretKey {
302    /// Create a new SecretKey from hex string
303    pub fn from_hex(hex: String) -> Result<Self, FfiError> {
304        // Validate hex string length (should be 64 characters for 32 bytes)
305        if hex.len() != 64 {
306            return Err(FfiError::internal(
307                "Secret key hex must be exactly 64 characters (32 bytes)",
308            ));
309        }
310
311        // Validate hex format
312        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
313            return Err(FfiError::internal(
314                "Secret key hex contains invalid characters",
315            ));
316        }
317
318        Ok(Self { hex })
319    }
320
321    /// Generate a random secret key
322    pub fn random() -> Self {
323        use cdk::nuts::SecretKey as CdkSecretKey;
324        let secret_key = CdkSecretKey::generate();
325        Self {
326            hex: secret_key.to_secret_hex(),
327        }
328    }
329}
330
331impl TryFrom<SecretKey> for cdk::nuts::SecretKey {
332    type Error = FfiError;
333
334    fn try_from(key: SecretKey) -> Result<Self, Self::Error> {
335        cdk::nuts::SecretKey::from_hex(&key.hex)
336            .map_err(|e| FfiError::internal(format!("Invalid secret key: {}", e)))
337    }
338}
339
340impl From<cdk::nuts::SecretKey> for SecretKey {
341    fn from(key: cdk::nuts::SecretKey) -> Self {
342        Self {
343            hex: key.to_secret_hex(),
344        }
345    }
346}
347
348/// FFI-compatible Receive options
349#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
350pub struct ReceiveOptions {
351    /// Amount split target
352    pub amount_split_target: SplitTarget,
353    /// P2PK signing keys
354    #[serde(default)]
355    pub p2pk_signing_keys: Vec<SecretKey>,
356    /// Preimages for HTLC conditions
357    pub preimages: Vec<String>,
358    /// Metadata
359    pub metadata: HashMap<String, String>,
360}
361
362impl Default for ReceiveOptions {
363    fn default() -> Self {
364        Self {
365            amount_split_target: SplitTarget::None,
366            p2pk_signing_keys: Vec::new(),
367            preimages: Vec::new(),
368            metadata: HashMap::new(),
369        }
370    }
371}
372
373impl TryFrom<ReceiveOptions> for cdk::wallet::ReceiveOptions {
374    type Error = FfiError;
375
376    fn try_from(opts: ReceiveOptions) -> Result<Self, Self::Error> {
377        let p2pk_signing_keys = opts
378            .p2pk_signing_keys
379            .into_iter()
380            .map(TryInto::try_into)
381            .collect::<Result<Vec<_>, _>>()?;
382
383        Ok(cdk::wallet::ReceiveOptions {
384            amount_split_target: opts.amount_split_target.into(),
385            p2pk_signing_keys,
386            preimages: opts.preimages,
387            metadata: opts.metadata,
388        })
389    }
390}
391
392impl From<cdk::wallet::ReceiveOptions> for ReceiveOptions {
393    fn from(opts: cdk::wallet::ReceiveOptions) -> Self {
394        Self {
395            amount_split_target: opts.amount_split_target.into(),
396            p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(),
397            preimages: opts.preimages,
398            metadata: opts.metadata,
399        }
400    }
401}
402
403impl ReceiveOptions {
404    /// Convert ReceiveOptions to JSON string
405    pub fn to_json(&self) -> Result<String, FfiError> {
406        Ok(serde_json::to_string(self)?)
407    }
408}
409
410/// Decode ReceiveOptions from JSON string
411#[uniffi::export]
412pub fn decode_receive_options(json: String) -> Result<ReceiveOptions, FfiError> {
413    Ok(serde_json::from_str(&json)?)
414}
415
416/// Encode ReceiveOptions to JSON string
417#[uniffi::export]
418pub fn encode_receive_options(options: ReceiveOptions) -> Result<String, FfiError> {
419    Ok(serde_json::to_string(&options)?)
420}
421
422/// FFI-compatible NUT-13 restore options
423#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
424pub struct NUT13Options {
425    /// Number of blinded messages to request per batch
426    pub batch_size: u32,
427    /// Number of consecutive empty batches that terminate the scan
428    pub max_gap: u32,
429}
430
431impl Default for NUT13Options {
432    fn default() -> Self {
433        cdk::wallet::NUT13Options::default().into()
434    }
435}
436
437impl TryFrom<NUT13Options> for cdk::wallet::NUT13Options {
438    type Error = FfiError;
439
440    fn try_from(opts: NUT13Options) -> Result<Self, Self::Error> {
441        Ok(cdk::wallet::NUT13Options::new(
442            opts.batch_size,
443            opts.max_gap,
444        )?)
445    }
446}
447
448impl From<cdk::wallet::NUT13Options> for NUT13Options {
449    fn from(opts: cdk::wallet::NUT13Options) -> Self {
450        NUT13Options {
451            batch_size: opts.batch_size,
452            max_gap: opts.max_gap,
453        }
454    }
455}
456
457/// FFI-compatible PreparedSend
458///
459/// This wraps the data from a prepared send operation along with a reference
460/// to the wallet. The actual PreparedSend<'a> from cdk has a lifetime parameter
461/// that doesn't work with FFI, so we store the wallet and cached data separately.
462#[derive(uniffi::Object)]
463pub struct PreparedSend {
464    wallet: std::sync::Arc<cdk::Wallet>,
465    operation_id: uuid::Uuid,
466    amount: Amount,
467    options: cdk::wallet::SendOptions,
468    proofs_to_swap: cdk::nuts::Proofs,
469    proofs_to_send: cdk::nuts::Proofs,
470    swap_fee: Amount,
471    send_fee: Amount,
472}
473
474impl std::fmt::Debug for PreparedSend {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        f.debug_struct("PreparedSend")
477            .field("operation_id", &self.operation_id)
478            .field("amount", &self.amount)
479            .finish()
480    }
481}
482
483impl PreparedSend {
484    /// Create a new FFI PreparedSend from a cdk::wallet::PreparedSend and wallet
485    pub fn new(
486        wallet: std::sync::Arc<cdk::Wallet>,
487        prepared: &cdk::wallet::PreparedSend<'_>,
488    ) -> Self {
489        Self {
490            wallet,
491            operation_id: prepared.operation_id(),
492            amount: prepared.amount().into(),
493            options: prepared.options().clone(),
494            proofs_to_swap: prepared.proofs_to_swap().clone(),
495            proofs_to_send: prepared.proofs_to_send().clone(),
496            swap_fee: prepared.swap_fee().into(),
497            send_fee: prepared.send_fee().into(),
498        }
499    }
500}
501
502#[uniffi::export(async_runtime = "tokio")]
503impl PreparedSend {
504    /// Get the operation ID for this prepared send
505    pub fn operation_id(&self) -> String {
506        self.operation_id.to_string()
507    }
508
509    /// Get the amount to send
510    pub fn amount(&self) -> Amount {
511        self.amount
512    }
513
514    /// Get the proofs that will be used
515    pub fn proofs(&self) -> Proofs {
516        let mut all_proofs: Vec<_> = self
517            .proofs_to_swap
518            .iter()
519            .cloned()
520            .map(|p| p.into())
521            .collect();
522        all_proofs.extend(self.proofs_to_send.iter().cloned().map(|p| p.into()));
523        all_proofs
524    }
525
526    /// Get the total fee for this send operation
527    pub fn fee(&self) -> Amount {
528        Amount::new(self.swap_fee.value + self.send_fee.value)
529    }
530
531    /// Confirm the prepared send and create a token
532    pub async fn confirm(
533        self: std::sync::Arc<Self>,
534        memo: Option<String>,
535    ) -> Result<Token, FfiError> {
536        let send_memo = memo.map(|m| cdk::wallet::SendMemo::for_token(&m));
537        let token = self
538            .wallet
539            .confirm_send(
540                self.operation_id,
541                self.amount.into(),
542                self.options.clone(),
543                self.proofs_to_swap.clone(),
544                self.proofs_to_send.clone(),
545                self.swap_fee.into(),
546                self.send_fee.into(),
547                send_memo,
548            )
549            .await?;
550
551        Ok(token.into())
552    }
553
554    /// Cancel the prepared send operation
555    pub async fn cancel(self: std::sync::Arc<Self>) -> Result<(), FfiError> {
556        self.wallet
557            .cancel_send(
558                self.operation_id,
559                self.proofs_to_swap.clone(),
560                self.proofs_to_send.clone(),
561            )
562            .await?;
563        Ok(())
564    }
565}
566
567/// FFI-compatible FinalizedMelt result
568#[derive(Debug, Clone, uniffi::Record)]
569pub struct FinalizedMelt {
570    pub quote_id: String,
571    pub state: super::quote::QuoteState,
572    pub preimage: Option<String>,
573    pub change: Option<Proofs>,
574    pub amount: Amount,
575    pub fee_paid: Amount,
576}
577
578impl From<cdk_common::common::FinalizedMelt> for FinalizedMelt {
579    fn from(finalized: cdk_common::common::FinalizedMelt) -> Self {
580        Self {
581            quote_id: finalized.quote_id().to_string(),
582            state: finalized.state().into(),
583            preimage: finalized.payment_proof().map(|s: &str| s.to_string()),
584            change: finalized
585                .change()
586                .map(|proofs| proofs.iter().cloned().map(|p| p.into()).collect()),
587            amount: finalized.amount().into(),
588            fee_paid: finalized.fee_paid().into(),
589        }
590    }
591}
592
593/// A pending async melt accepted by the mint.
594///
595/// FFI callers receive this handle when the mint accepts a melt for background
596/// processing. Call [`PendingMelt::wait`] from a background task/coroutine to
597/// poll existing wallet recovery until the melt settles.
598///
599/// Mobile apps should also call [`crate::Wallet::recover_incomplete_sagas`] or
600/// [`crate::Wallet::finalize_pending_melts`] on startup/resume, because
601/// operating systems may suspend or cancel long-running background waits.
602#[derive(uniffi::Object)]
603pub struct PendingMelt {
604    wallet: Arc<cdk::Wallet>,
605    quote_id: String,
606    operation_id: uuid::Uuid,
607    payment_method: cdk_common::PaymentMethod,
608}
609
610impl std::fmt::Debug for PendingMelt {
611    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612        f.debug_struct("PendingMelt")
613            .field("operation_id", &self.operation_id)
614            .field("quote_id", &self.quote_id)
615            .finish()
616    }
617}
618
619#[uniffi::export(async_runtime = "tokio")]
620impl PendingMelt {
621    /// Quote ID for this pending melt.
622    pub fn quote_id(&self) -> String {
623        self.quote_id.clone()
624    }
625
626    /// Operation ID for this pending melt saga.
627    pub fn operation_id(&self) -> String {
628        self.operation_id.to_string()
629    }
630
631    /// Wait for this pending melt to complete.
632    ///
633    /// This method polls the wallet's existing melt recovery path until the
634    /// pending saga finalizes or fails.
635    ///
636    /// This can wait for an extended period. Swift/Kotlin callers should run it
637    /// in a cancellable background task or coroutine, not directly in UI
638    /// control flow. If the app is suspended or killed before this returns,
639    /// call `Wallet::recover_incomplete_sagas()` or
640    /// `Wallet::finalize_pending_melts()` after restart/resume.
641    pub async fn wait(&self) -> Result<FinalizedMelt, FfiError> {
642        let finalized = self
643            .wallet
644            .wait_pending_melt(
645                self.operation_id,
646                &self.quote_id,
647                self.payment_method.clone(),
648            )
649            .await?;
650
651        Ok(finalized.into())
652    }
653}
654
655/// Result of async-preferred melt confirmation.
656///
657/// `Paid` means the melt finalized during confirmation. `Pending` means the
658/// mint accepted the melt for asynchronous processing; call
659/// [`PendingMelt::wait`] to complete the normal app flow.
660#[derive(Debug, Clone, uniffi::Enum)]
661pub enum MeltConfirmOutcome {
662    /// Melt finalized during confirmation.
663    Paid { finalized: FinalizedMelt },
664    /// Mint accepted async melt processing and the payment is still pending.
665    Pending { pending: Arc<PendingMelt> },
666}
667
668/// FFI-compatible PreparedMelt
669///
670/// This wraps the data from a prepared melt operation along with a reference
671/// to the wallet. The actual PreparedMelt<'a> from cdk has a lifetime parameter
672/// that doesn't work with FFI, so we store the wallet and cached data separately.
673#[derive(uniffi::Object)]
674pub struct PreparedMelt {
675    wallet: Arc<cdk::Wallet>,
676    operation_id: uuid::Uuid,
677    quote: cdk_common::wallet::MeltQuote,
678    proofs: cdk::nuts::Proofs,
679    proofs_to_swap: cdk::nuts::Proofs,
680    swap_fee: Amount,
681    input_fee: Amount,
682    input_fee_without_swap: Amount,
683    metadata: HashMap<String, String>,
684}
685
686impl std::fmt::Debug for PreparedMelt {
687    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688        f.debug_struct("PreparedMelt")
689            .field("operation_id", &self.operation_id)
690            .field("quote_id", &self.quote.id)
691            .field("amount", &self.quote.amount)
692            .finish()
693    }
694}
695
696impl PreparedMelt {
697    /// Create a new FFI PreparedMelt from a cdk::wallet::PreparedMelt and wallet
698    pub fn new(wallet: Arc<cdk::Wallet>, prepared: &cdk::wallet::PreparedMelt<'_>) -> Self {
699        Self {
700            wallet,
701            operation_id: prepared.operation_id(),
702            quote: prepared.quote().clone(),
703            proofs: prepared.proofs().clone(),
704            proofs_to_swap: prepared.proofs_to_swap().clone(),
705            swap_fee: prepared.swap_fee().into(),
706            input_fee: prepared.input_fee().into(),
707            input_fee_without_swap: prepared.input_fee_without_swap().into(),
708            metadata: prepared.metadata().clone(),
709        }
710    }
711
712    async fn confirm_prefer_async_with_options(
713        &self,
714        options: MeltConfirmOptions,
715    ) -> Result<MeltConfirmOutcome, FfiError> {
716        let outcome = self
717            .wallet
718            .confirm_prepared_melt_prefer_async_with_options(
719                self.operation_id,
720                self.quote.clone(),
721                self.proofs.clone(),
722                self.proofs_to_swap.clone(),
723                self.input_fee.into(),
724                self.input_fee_without_swap.into(),
725                self.metadata.clone(),
726                options.into(),
727            )
728            .await?;
729
730        match outcome {
731            cdk::wallet::MeltOutcome::Paid(finalized) => Ok(MeltConfirmOutcome::Paid {
732                finalized: finalized.into(),
733            }),
734            cdk::wallet::MeltOutcome::Pending(_) => Ok(MeltConfirmOutcome::Pending {
735                pending: Arc::new(PendingMelt {
736                    wallet: Arc::clone(&self.wallet),
737                    quote_id: self.quote.id.clone(),
738                    operation_id: self.operation_id,
739                    payment_method: self.quote.payment_method.clone(),
740                }),
741            }),
742        }
743    }
744}
745
746#[uniffi::export(async_runtime = "tokio")]
747impl PreparedMelt {
748    /// Get the operation ID for this prepared melt
749    pub fn operation_id(&self) -> String {
750        self.operation_id.to_string()
751    }
752
753    /// Get the quote ID
754    pub fn quote_id(&self) -> String {
755        self.quote.id.clone()
756    }
757
758    /// Get the amount to be melted
759    pub fn amount(&self) -> Amount {
760        self.quote.amount.into()
761    }
762
763    /// Get the fee reserve from the quote
764    pub fn fee_reserve(&self) -> Amount {
765        self.quote.fee_reserve.into()
766    }
767
768    /// Get the swap fee
769    pub fn swap_fee(&self) -> Amount {
770        self.swap_fee
771    }
772
773    /// Get the input fee
774    pub fn input_fee(&self) -> Amount {
775        self.input_fee
776    }
777
778    /// Get the total fee (swap fee + input fee)
779    pub fn total_fee(&self) -> Amount {
780        Amount::new(self.swap_fee.value + self.input_fee.value)
781    }
782
783    /// Returns true if a swap would be performed (proofs_to_swap is not empty)
784    pub fn requires_swap(&self) -> bool {
785        !self.proofs_to_swap.is_empty()
786    }
787
788    /// Get the total fee if swap is performed (current default behavior)
789    pub fn total_fee_with_swap(&self) -> Amount {
790        Amount::new(self.swap_fee.value + self.input_fee.value)
791    }
792
793    /// Get the input fee if swap is skipped (fee on all proofs sent directly)
794    pub fn input_fee_without_swap(&self) -> Amount {
795        self.input_fee_without_swap
796    }
797
798    /// Get the fee savings from skipping the swap
799    pub fn fee_savings_without_swap(&self) -> Amount {
800        let total_with = self.swap_fee.value + self.input_fee.value;
801        let total_without = self.input_fee_without_swap.value;
802        if total_with > total_without {
803            Amount::new(total_with - total_without)
804        } else {
805            Amount::new(0)
806        }
807    }
808
809    /// Get the expected change amount if swap is skipped
810    pub fn change_amount_without_swap(&self) -> Amount {
811        use cdk::nuts::nut00::ProofsMethods;
812        let all_proofs_total = self.proofs.total_amount().unwrap_or(cdk::Amount::ZERO)
813            + self
814                .proofs_to_swap
815                .total_amount()
816                .unwrap_or(cdk::Amount::ZERO);
817        let needed =
818            self.quote.amount + self.quote.fee_reserve + self.input_fee_without_swap.into();
819        all_proofs_total
820            .checked_sub(needed)
821            .map(|a| a.into())
822            .unwrap_or(Amount::new(0))
823    }
824
825    /// Get the proofs that will be used
826    pub fn proofs(&self) -> Proofs {
827        self.proofs.iter().cloned().map(|p| p.into()).collect()
828    }
829
830    /// Confirm the prepared melt and execute the payment
831    pub async fn confirm(&self) -> Result<FinalizedMelt, FfiError> {
832        self.confirm_with_options(MeltConfirmOptions::default())
833            .await
834    }
835
836    /// Confirm the prepared melt with custom options
837    pub async fn confirm_with_options(
838        &self,
839        options: MeltConfirmOptions,
840    ) -> Result<FinalizedMelt, FfiError> {
841        let finalized = self
842            .wallet
843            .confirm_prepared_melt_with_options(
844                self.operation_id,
845                self.quote.clone(),
846                self.proofs.clone(),
847                self.proofs_to_swap.clone(),
848                self.input_fee.into(),
849                self.input_fee_without_swap.into(),
850                self.metadata.clone(),
851                options.into(),
852            )
853            .await?;
854
855        Ok(finalized.into())
856    }
857
858    /// Confirm the prepared melt using NUT-05 async support when the mint accepts it.
859    ///
860    /// If the melt completes immediately, this returns
861    /// `MeltConfirmOutcome::Paid`. If the mint accepts the payment for
862    /// background processing, this returns `MeltConfirmOutcome::Pending` with a
863    /// `PendingMelt` handle.
864    ///
865    /// FFI callers should call `PendingMelt::wait()` from a background
866    /// task/coroutine to poll for completion. Mobile apps should also call
867    /// `recover_incomplete_sagas()` or `finalize_pending_melts()` on
868    /// startup/resume, because operating systems may suspend or cancel
869    /// long-running background waits.
870    pub async fn confirm_prefer_async(&self) -> Result<MeltConfirmOutcome, FfiError> {
871        self.confirm_prefer_async_with_options(MeltConfirmOptions::default())
872            .await
873    }
874
875    /// Cancel the prepared melt and release reserved proofs
876    pub async fn cancel(&self) -> Result<(), FfiError> {
877        self.wallet
878            .cancel_prepared_melt(
879                self.operation_id,
880                self.proofs.clone(),
881                self.proofs_to_swap.clone(),
882            )
883            .await?;
884        Ok(())
885    }
886}
887
888/// FFI-compatible MeltOptions
889#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
890pub enum MeltOptions {
891    /// MPP (Multi-Part Payments) options
892    Mpp { amount: Amount },
893    /// Amountless options
894    Amountless { amount_msat: Amount },
895}
896
897impl From<MeltOptions> for cdk::nuts::MeltOptions {
898    fn from(opts: MeltOptions) -> Self {
899        match opts {
900            MeltOptions::Mpp { amount } => {
901                let cdk_amount: cdk::Amount = amount.into();
902                cdk::nuts::MeltOptions::new_mpp(cdk_amount)
903            }
904            MeltOptions::Amountless { amount_msat } => {
905                let cdk_amount: cdk::Amount = amount_msat.into();
906                cdk::nuts::MeltOptions::new_amountless(cdk_amount)
907            }
908        }
909    }
910}
911
912impl From<cdk::nuts::MeltOptions> for MeltOptions {
913    fn from(opts: cdk::nuts::MeltOptions) -> Self {
914        match opts {
915            cdk::nuts::MeltOptions::Mpp { mpp } => MeltOptions::Mpp {
916                amount: mpp.amount.into(),
917            },
918            cdk::nuts::MeltOptions::Amountless { amountless } => MeltOptions::Amountless {
919                amount_msat: amountless.amount_msat.into(),
920            },
921        }
922    }
923}
924
925/// Restored Data
926#[derive(Debug, Clone, uniffi::Record)]
927pub struct Restored {
928    pub spent: Amount,
929    pub unspent: Amount,
930    pub pending: Amount,
931}
932
933impl From<cdk_common::wallet::Restored> for Restored {
934    fn from(restored: cdk_common::wallet::Restored) -> Self {
935        Self {
936            spent: restored.spent.into(),
937            unspent: restored.unspent.into(),
938            pending: restored.pending.into(),
939        }
940    }
941}
942
943/// Report of wallet saga recovery operations.
944#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, uniffi::Record)]
945pub struct RecoveryReport {
946    /// Operations successfully completed after crash.
947    pub recovered: u64,
948    /// Operations rolled back and resources released.
949    pub compensated: u64,
950    /// Operations still pending and left for a later retry.
951    pub skipped: u64,
952    /// Operations that could not be recovered.
953    pub failed: u64,
954}
955
956impl From<cdk::wallet::RecoveryReport> for RecoveryReport {
957    fn from(report: cdk::wallet::RecoveryReport) -> Self {
958        Self {
959            recovered: report.recovered as u64,
960            compensated: report.compensated as u64,
961            skipped: report.skipped as u64,
962            failed: report.failed as u64,
963        }
964    }
965}
966
967/// FFI-compatible options for confirming a melt operation
968#[derive(Debug, Clone, Default, Serialize, Deserialize, uniffi::Record)]
969pub struct MeltConfirmOptions {
970    /// Skip the pre-melt swap and send proofs directly to melt.
971    /// When true, saves swap input fees but gets change from melt instead.
972    pub skip_swap: bool,
973}
974
975impl From<MeltConfirmOptions> for cdk::wallet::MeltConfirmOptions {
976    fn from(opts: MeltConfirmOptions) -> Self {
977        cdk::wallet::MeltConfirmOptions {
978            skip_swap: opts.skip_swap,
979        }
980    }
981}
982
983impl From<cdk::wallet::MeltConfirmOptions> for MeltConfirmOptions {
984    fn from(opts: cdk::wallet::MeltConfirmOptions) -> Self {
985        Self {
986            skip_swap: opts.skip_swap,
987        }
988    }
989}
990
991/// FFI-compatible WalletKey
992#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
993pub struct WalletKey {
994    /// Mint Url
995    pub mint_url: MintUrl,
996    /// Currency Unit
997    pub unit: CurrencyUnit,
998}
999
1000impl TryFrom<WalletKey> for cdk::WalletKey {
1001    type Error = FfiError;
1002
1003    fn try_from(value: WalletKey) -> Result<Self, Self::Error> {
1004        Ok(Self {
1005            mint_url: value.mint_url.try_into()?,
1006            unit: value.unit.into(),
1007        })
1008    }
1009}
1010
1011impl From<cdk::WalletKey> for WalletKey {
1012    fn from(value: cdk::WalletKey) -> Self {
1013        Self {
1014            mint_url: value.mint_url.into(),
1015            unit: value.unit.into(),
1016        }
1017    }
1018}