Skip to main content

cdk_common/wallet/
mod.rs

1//! Wallet Types
2
3use std::collections::HashMap;
4use std::fmt;
5use std::str::FromStr;
6
7use async_trait::async_trait;
8use bitcoin::bip32::DerivationPath;
9use bitcoin::hashes::{sha256, Hash, HashEngine};
10use cashu::amount::{FeeAndAmounts, KeysetFeeAndAmounts, SplitTarget};
11use cashu::nuts::nut07::ProofState;
12use cashu::nuts::nut18::PaymentRequest;
13use cashu::nuts::{AuthProof, Keys};
14use cashu::util::hex;
15use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use crate::mint_quote::quote_state_from_amounts;
20use crate::mint_url::MintUrl;
21use crate::nuts::{
22    CurrencyUnit, Id, MeltQuoteState, MintQuoteState, SecretKey, SpendingConditions, State,
23};
24use crate::{Amount, Error};
25
26pub mod saga;
27
28pub use saga::{
29    IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
30    ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
31    SwapSagaState, WalletSaga, WalletSagaState,
32};
33
34/// Wallet Key
35#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36pub struct WalletKey {
37    /// Mint Url
38    pub mint_url: MintUrl,
39    /// Currency Unit
40    pub unit: CurrencyUnit,
41}
42
43impl fmt::Display for WalletKey {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
46    }
47}
48
49impl WalletKey {
50    /// Create new [`WalletKey`]
51    pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
52        Self { mint_url, unit }
53    }
54}
55
56/// Proof info
57#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ProofInfo {
59    /// Proof
60    pub proof: Proof,
61    /// y
62    pub y: PublicKey,
63    /// Mint Url
64    pub mint_url: MintUrl,
65    /// Proof State
66    pub state: State,
67    /// Proof Spending Conditions
68    pub spending_condition: Option<SpendingConditions>,
69    /// Unit
70    pub unit: CurrencyUnit,
71    /// Operation ID that is using/spending this proof
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub used_by_operation: Option<Uuid>,
74    /// Operation ID that created this proof
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub created_by_operation: Option<Uuid>,
77}
78
79impl ProofInfo {
80    /// Create new [`ProofInfo`]
81    pub fn new(
82        proof: Proof,
83        mint_url: MintUrl,
84        state: State,
85        unit: CurrencyUnit,
86    ) -> Result<Self, Error> {
87        let y = proof.y()?;
88
89        let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
90
91        Ok(Self {
92            proof,
93            y,
94            mint_url,
95            state,
96            spending_condition,
97            unit,
98            used_by_operation: None,
99            created_by_operation: None,
100        })
101    }
102
103    /// Create new [`ProofInfo`] with operation tracking
104    pub fn new_with_operations(
105        proof: Proof,
106        mint_url: MintUrl,
107        state: State,
108        unit: CurrencyUnit,
109        used_by_operation: Option<Uuid>,
110        created_by_operation: Option<Uuid>,
111    ) -> Result<Self, Error> {
112        let y = proof.y()?;
113
114        let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
115
116        Ok(Self {
117            proof,
118            y,
119            mint_url,
120            state,
121            spending_condition,
122            unit,
123            used_by_operation,
124            created_by_operation,
125        })
126    }
127
128    /// Check if [`Proof`] matches conditions
129    pub fn matches_conditions(
130        &self,
131        mint_url: &Option<MintUrl>,
132        unit: &Option<CurrencyUnit>,
133        state: &Option<Vec<State>>,
134        spending_conditions: &Option<Vec<SpendingConditions>>,
135    ) -> bool {
136        if let Some(mint_url) = mint_url {
137            if mint_url.ne(&self.mint_url) {
138                return false;
139            }
140        }
141
142        if let Some(unit) = unit {
143            if unit.ne(&self.unit) {
144                return false;
145            }
146        }
147
148        if let Some(state) = state {
149            if !state.contains(&self.state) {
150                return false;
151            }
152        }
153
154        if let Some(spending_conditions) = spending_conditions {
155            match &self.spending_condition {
156                None => {
157                    if !spending_conditions.is_empty() {
158                        return false;
159                    }
160                }
161                Some(s) => {
162                    if !spending_conditions.contains(s) {
163                        return false;
164                    }
165                }
166            }
167        }
168
169        true
170    }
171}
172
173/// Mint Quote Info
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct MintQuote {
176    /// Quote id
177    pub id: String,
178    /// Mint Url
179    pub mint_url: MintUrl,
180    /// Payment method
181    pub payment_method: PaymentMethod,
182    /// Requested or fixed quote amount, when defined by the payment method.
183    ///
184    /// Variable-amount methods such as onchain leave this unset and track
185    /// funds through `amount_paid` and `amount_issued`.
186    pub amount: Option<Amount>,
187    /// Unit of quote
188    pub unit: CurrencyUnit,
189    /// Quote payment request e.g. bolt11
190    pub request: String,
191    /// Quote state
192    pub state: MintQuoteState,
193    /// Expiration time of quote
194    pub expiry: u64,
195    /// Secretkey for signing mint quotes [NUT-20]
196    pub secret_key: Option<SecretKey>,
197    /// Amount minted
198    #[serde(default)]
199    pub amount_issued: Amount,
200    /// Amount paid to the mint for the quote
201    #[serde(default)]
202    pub amount_paid: Amount,
203    /// Estimated confirmation target in blocks for onchain quotes
204    pub estimated_blocks: Option<u32>,
205    /// Operation ID that has reserved this quote (for saga pattern)
206    #[serde(default)]
207    pub used_by_operation: Option<String>,
208    /// Version for optimistic locking
209    #[serde(default)]
210    pub version: u32,
211}
212
213/// Melt Quote Info
214#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
215pub struct MeltQuote {
216    /// Quote id
217    pub id: String,
218    /// Mint Url
219    pub mint_url: Option<MintUrl>,
220    /// Quote unit
221    pub unit: CurrencyUnit,
222    /// Quote amount
223    pub amount: Amount,
224    /// Quote Payment request e.g. bolt11
225    pub request: String,
226    /// Quote fee reserve
227    pub fee_reserve: Amount,
228    /// Quote state
229    pub state: MeltQuoteState,
230    /// Expiration time of quote
231    pub expiry: u64,
232    /// Payment proof (e.g. Lightning preimage or onchain outpoint)
233    #[serde(alias = "payment_preimage")]
234    pub payment_proof: Option<String>,
235    /// Estimated confirmation target in blocks for onchain quotes
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub estimated_blocks: Option<u32>,
238    /// Selected fee option index for onchain quotes
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub fee_index: Option<u32>,
241    /// Payment method
242    pub payment_method: PaymentMethod,
243    /// Operation ID that has reserved this quote (for saga pattern)
244    #[serde(default)]
245    pub used_by_operation: Option<String>,
246    /// Version for optimistic locking
247    #[serde(default)]
248    pub version: u32,
249}
250
251impl MintQuote {
252    /// Create a new MintQuote
253    #[allow(clippy::too_many_arguments)]
254    pub fn new(
255        id: String,
256        mint_url: MintUrl,
257        payment_method: PaymentMethod,
258        amount: Option<Amount>,
259        unit: CurrencyUnit,
260        request: String,
261        expiry: u64,
262        secret_key: Option<SecretKey>,
263    ) -> Self {
264        Self {
265            id,
266            mint_url,
267            payment_method,
268            amount,
269            unit,
270            request,
271            state: MintQuoteState::Unpaid,
272            expiry,
273            secret_key,
274            amount_issued: Amount::ZERO,
275            amount_paid: Amount::ZERO,
276            estimated_blocks: None,
277            used_by_operation: None,
278            version: 0,
279        }
280    }
281
282    /// Calculate the total amount including any fees
283    pub fn total_amount(&self) -> Amount {
284        self.amount_paid
285    }
286
287    /// Derive quote state from the tracked payment and issuance counters.
288    pub fn state_from_amounts(&self) -> MintQuoteState {
289        quote_state_from_amounts(self.amount_paid, self.amount_issued)
290    }
291
292    /// Update quote state from the tracked payment and issuance counters.
293    pub fn update_state_from_amounts(&mut self) {
294        self.state = self.state_from_amounts();
295    }
296
297    /// Check if the quote has expired
298    pub fn is_expired(&self, current_time: u64) -> bool {
299        current_time > self.expiry
300    }
301
302    /// Amount that can be minted
303    pub fn amount_mintable(&self) -> Amount {
304        if self.payment_method == PaymentMethod::BOLT11 {
305            // BOLT11 is all-or-nothing: mint full amount when state is Paid
306            if self.state == MintQuoteState::Paid {
307                self.amount.unwrap_or(Amount::ZERO)
308            } else {
309                Amount::ZERO
310            }
311        } else {
312            // Other payment methods track incremental payments
313            self.amount_paid
314                .checked_sub(self.amount_issued)
315                .unwrap_or(Amount::ZERO)
316        }
317    }
318}
319
320/// Amounts recovered during a restore operation
321#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
322pub struct Restored {
323    /// Amount in the restore that has already been spent
324    pub spent: Amount,
325    /// Amount restored that is unspent
326    pub unspent: Amount,
327    /// Amount restored that is pending
328    pub pending: Amount,
329}
330
331/// Options for [`crate::wallet::Wallet::restore_with_opts`].
332///
333/// Defaults match the NUT-13 spec recommendation
334/// (<https://github.com/cashubtc/nuts/blob/main/13.md#generate-blindedmessages>):
335/// a batch of 100 blinded messages and three consecutive empty batches to
336/// signal end-of-history. Callers that need more conservative pacing or
337/// different gap tolerance can override either field.
338#[derive(Debug, Clone)]
339pub struct NUT13Options {
340    /// Number of blinded messages to request per batch.
341    pub batch_size: u32,
342    /// Number of consecutive empty batches that terminate the scan.
343    pub max_gap: u32,
344}
345
346impl Default for NUT13Options {
347    fn default() -> Self {
348        Self {
349            batch_size: Self::DEFAULT_BATCH_SIZE,
350            max_gap: Self::DEFAULT_MAX_GAP,
351        }
352    }
353}
354
355impl NUT13Options {
356    /// NUT-13 default restore batch size.
357    pub const DEFAULT_BATCH_SIZE: u32 = 100;
358
359    /// NUT-13 default restore gap limit.
360    pub const DEFAULT_MAX_GAP: u32 = 3;
361
362    /// Create new NUT-13 restore options.
363    pub fn new(batch_size: u32, max_gap: u32) -> Result<Self, Error> {
364        let opts = Self {
365            batch_size,
366            max_gap,
367        };
368        opts.validate()?;
369        Ok(opts)
370    }
371
372    pub(crate) fn validate(&self) -> Result<(), Error> {
373        if self.batch_size == 0 {
374            return Err(Error::InvalidNut13Options {
375                field: "batch_size",
376                reason: "must be greater than zero",
377            });
378        }
379
380        if self.max_gap == 0 {
381            return Err(Error::InvalidNut13Options {
382                field: "max_gap",
383                reason: "must be greater than zero",
384            });
385        }
386
387        Ok(())
388    }
389}
390
391/// Send options
392#[derive(Clone, Default)]
393pub struct SendOptions {
394    /// Memo
395    pub memo: Option<SendMemo>,
396    /// Spending conditions
397    pub conditions: Option<SpendingConditions>,
398    /// Amount split target
399    pub amount_split_target: SplitTarget,
400    /// Send kind
401    pub send_kind: SendKind,
402    /// Include fee
403    pub include_fee: bool,
404    /// Maximum number of proofs to include in the token
405    pub max_proofs: Option<usize>,
406    /// Metadata
407    pub metadata: HashMap<String, String>,
408    /// Use P2BK (NUT-28)
409    pub use_p2bk: bool,
410    /// Signing keys for P2PK-locked input proofs; auto-detected from the wallet keyring if omitted
411    pub p2pk_signing_keys: Vec<SecretKey>,
412    /// How P2PK-locked input proofs should be handled during send
413    pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
414}
415
416impl fmt::Debug for SendOptions {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        f.debug_struct("SendOptions")
419            .field("memo", &self.memo)
420            .field("conditions", &self.conditions)
421            .field("amount_split_target", &self.amount_split_target)
422            .field("send_kind", &self.send_kind)
423            .field("include_fee", &self.include_fee)
424            .field("max_proofs", &self.max_proofs)
425            .field("metadata", &self.metadata)
426            .field("use_p2bk", &self.use_p2bk)
427            .field("p2pk_signing_keys", &"[redacted]")
428            .field(
429                "p2pk_locked_proof_send_mode",
430                &self.p2pk_locked_proof_send_mode,
431            )
432            .finish()
433    }
434}
435
436/// Send behavior for selected P2PK-locked input proofs
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
438pub enum P2PKLockedProofSendMode {
439    /// Swap locked proofs into fresh proofs before creating the token
440    #[default]
441    Swap,
442    /// Sign locked proofs and include them directly in the token
443    SignAndSend,
444}
445
446/// Send memo
447#[derive(Debug, Clone)]
448pub struct SendMemo {
449    /// Memo
450    pub memo: String,
451    /// Include memo in token
452    pub include_memo: bool,
453}
454
455impl SendMemo {
456    /// Create a new send memo
457    pub fn for_token(memo: &str) -> Self {
458        Self {
459            memo: memo.to_string(),
460            include_memo: true,
461        }
462    }
463}
464
465/// Receive options
466#[derive(Clone, Default)]
467pub struct ReceiveOptions {
468    /// Amount split target
469    pub amount_split_target: SplitTarget,
470    /// P2PK signing keys
471    pub p2pk_signing_keys: Vec<SecretKey>,
472    /// Preimages
473    pub preimages: Vec<String>,
474    /// Metadata
475    pub metadata: HashMap<String, String>,
476}
477
478impl fmt::Debug for ReceiveOptions {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        f.debug_struct("ReceiveOptions")
481            .field("amount_split_target", &self.amount_split_target)
482            .field("p2pk_signing_keys", &"[redacted]")
483            .field("preimages", &self.preimages)
484            .field("metadata", &self.metadata)
485            .finish()
486    }
487}
488
489/// Send Kind
490#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
491pub enum SendKind {
492    #[default]
493    /// Allow online swap before send if wallet does not have exact amount
494    OnlineExact,
495    /// Prefer offline send if difference is less then tolerance
496    OnlineTolerance(Amount),
497    /// Wallet cannot do an online swap and selected proof must be exactly send amount
498    OfflineExact,
499    /// Wallet must remain offline but can over pay if below tolerance
500    OfflineTolerance(Amount),
501}
502
503impl SendKind {
504    /// Check if send kind is online
505    pub fn is_online(&self) -> bool {
506        matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
507    }
508
509    /// Check if send kind is offline
510    pub fn is_offline(&self) -> bool {
511        matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
512    }
513
514    /// Check if send kind is exact
515    pub fn is_exact(&self) -> bool {
516        matches!(self, Self::OnlineExact | Self::OfflineExact)
517    }
518
519    /// Check if send kind has tolerance
520    pub fn has_tolerance(&self) -> bool {
521        matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
522    }
523}
524
525/// Wallet Transaction
526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
527pub struct Transaction {
528    /// Mint Url
529    pub mint_url: MintUrl,
530    /// Transaction direction
531    pub direction: TransactionDirection,
532    /// Amount
533    pub amount: Amount,
534    /// Fee
535    pub fee: Amount,
536    /// Currency Unit
537    pub unit: CurrencyUnit,
538    /// Proof Ys
539    pub ys: Vec<PublicKey>,
540    /// Unix timestamp
541    pub timestamp: u64,
542    /// Memo
543    pub memo: Option<String>,
544    /// User-defined metadata
545    pub metadata: HashMap<String, String>,
546    /// Quote ID if this is a mint or melt transaction
547    pub quote_id: Option<String>,
548    /// Payment request (e.g., BOLT11 invoice, BOLT12 offer)
549    pub payment_request: Option<String>,
550    /// Payment proof (e.g., preimage for Lightning melt transactions)
551    #[serde(alias = "payment_preimage")]
552    pub payment_proof: Option<String>,
553    /// Payment method (e.g., Bolt11, Bolt12) for mint/melt transactions
554    #[serde(default)]
555    pub payment_method: Option<PaymentMethod>,
556    /// Saga ID if this transaction was part of a saga
557    #[serde(default)]
558    pub saga_id: Option<Uuid>,
559}
560
561impl Transaction {
562    /// Transaction ID
563    pub fn id(&self) -> TransactionId {
564        TransactionId::new(self.ys.clone())
565    }
566
567    /// Check if transaction matches conditions
568    pub fn matches_conditions(
569        &self,
570        mint_url: &Option<MintUrl>,
571        direction: &Option<TransactionDirection>,
572        unit: &Option<CurrencyUnit>,
573    ) -> bool {
574        if let Some(mint_url) = mint_url {
575            if &self.mint_url != mint_url {
576                return false;
577            }
578        }
579        if let Some(direction) = direction {
580            if &self.direction != direction {
581                return false;
582            }
583        }
584        if let Some(unit) = unit {
585            if &self.unit != unit {
586                return false;
587            }
588        }
589        true
590    }
591}
592
593impl PartialOrd for Transaction {
594    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
595        Some(self.cmp(other))
596    }
597}
598
599impl Ord for Transaction {
600    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
601        self.timestamp
602            .cmp(&other.timestamp)
603            .reverse()
604            .then_with(|| self.id().cmp(&other.id()))
605    }
606}
607
608/// Transaction Direction
609#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
610pub enum TransactionDirection {
611    /// Incoming transaction (i.e., receive or mint)
612    Incoming,
613    /// Outgoing transaction (i.e., send or melt)
614    Outgoing,
615}
616
617impl std::fmt::Display for TransactionDirection {
618    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619        match self {
620            TransactionDirection::Incoming => write!(f, "Incoming"),
621            TransactionDirection::Outgoing => write!(f, "Outgoing"),
622        }
623    }
624}
625
626impl FromStr for TransactionDirection {
627    type Err = Error;
628
629    fn from_str(value: &str) -> Result<Self, Self::Err> {
630        match value {
631            "Incoming" => Ok(Self::Incoming),
632            "Outgoing" => Ok(Self::Outgoing),
633            _ => Err(Error::InvalidTransactionDirection),
634        }
635    }
636}
637
638/// Transaction ID
639#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
640#[serde(transparent)]
641pub struct TransactionId([u8; 32]);
642
643impl TransactionId {
644    /// Create new [`TransactionId`]
645    pub fn new(ys: Vec<PublicKey>) -> Self {
646        let mut ys = ys;
647        ys.sort();
648        let mut hasher = sha256::Hash::engine();
649        for y in ys {
650            hasher.input(&y.to_bytes());
651        }
652        let hash = sha256::Hash::from_engine(hasher);
653        Self(hash.to_byte_array())
654    }
655
656    /// From proofs
657    pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
658        let ys = proofs
659            .iter()
660            .map(|proof| proof.y())
661            .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
662        Ok(Self::new(ys))
663    }
664
665    /// From bytes
666    pub fn from_bytes(bytes: [u8; 32]) -> Self {
667        Self(bytes)
668    }
669
670    /// From hex string
671    pub fn from_hex(value: &str) -> Result<Self, Error> {
672        let bytes = hex::decode(value)?;
673        if bytes.len() != 32 {
674            return Err(Error::InvalidTransactionId);
675        }
676        let mut array = [0u8; 32];
677        array.copy_from_slice(&bytes);
678        Ok(Self(array))
679    }
680
681    /// From slice
682    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
683        if slice.len() != 32 {
684            return Err(Error::InvalidTransactionId);
685        }
686        let mut array = [0u8; 32];
687        array.copy_from_slice(slice);
688        Ok(Self(array))
689    }
690
691    /// Get inner value
692    pub fn as_bytes(&self) -> &[u8; 32] {
693        &self.0
694    }
695
696    /// Get inner value as slice
697    pub fn as_slice(&self) -> &[u8] {
698        &self.0
699    }
700}
701
702impl std::fmt::Display for TransactionId {
703    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
704        write!(f, "{}", hex::encode(self.0))
705    }
706}
707
708impl FromStr for TransactionId {
709    type Err = Error;
710
711    fn from_str(value: &str) -> Result<Self, Self::Err> {
712        Self::from_hex(value)
713    }
714}
715
716impl TryFrom<Proofs> for TransactionId {
717    type Error = nut00::Error;
718
719    fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
720        Self::from_proofs(proofs)
721    }
722}
723
724/// Wallet operation kind
725#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "snake_case")]
727pub enum OperationKind {
728    /// Send operation
729    Send,
730    /// Receive operation
731    Receive,
732    /// Swap operation
733    Swap,
734    /// Mint operation
735    Mint,
736    /// Melt operation
737    Melt,
738}
739
740impl fmt::Display for OperationKind {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        match self {
743            OperationKind::Send => write!(f, "send"),
744            OperationKind::Receive => write!(f, "receive"),
745            OperationKind::Swap => write!(f, "swap"),
746            OperationKind::Mint => write!(f, "mint"),
747            OperationKind::Melt => write!(f, "melt"),
748        }
749    }
750}
751
752impl FromStr for OperationKind {
753    type Err = Error;
754
755    fn from_str(s: &str) -> Result<Self, Self::Err> {
756        match s {
757            "send" => Ok(OperationKind::Send),
758            "receive" => Ok(OperationKind::Receive),
759            "swap" => Ok(OperationKind::Swap),
760            "mint" => Ok(OperationKind::Mint),
761            "melt" => Ok(OperationKind::Melt),
762            _ => Err(Error::InvalidOperationKind),
763        }
764    }
765}
766
767/// Filter for keyset queries
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub enum KeysetFilter {
770    /// Only return active keysets
771    Active,
772    /// Return all keysets (active and inactive)
773    All,
774}
775
776/// Unified wallet trait providing a common interface for wallet operations.
777///
778/// This trait abstracts over different wallet implementations (CDK wallet, FFI
779/// wrappers, etc.) and provides a consistent interface for balance queries,
780/// minting, melting, keyset management, and other core wallet operations.
781///
782/// All domain types are associated types so each implementation can use its own
783/// type system (e.g. FFI-friendly records vs native Rust types).
784#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
785#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
786pub trait Wallet: Send + Sync {
787    /// Error type
788    type Error: std::error::Error + Send + Sync + 'static;
789    /// Amount type (e.g. `cdk_common::Amount` or FFI `Amount`)
790    type Amount: Clone + Send + Sync;
791    /// Mint URL type
792    type MintUrl: Clone + Send + Sync;
793    /// Currency unit type
794    type CurrencyUnit: Clone + Send + Sync;
795    /// Mint info type
796    type MintInfo: Clone + Send + Sync;
797    /// Keyset info type
798    type KeySetInfo: Clone + Send + Sync;
799    /// Mint quote type
800    type MintQuote: Clone + Send + Sync;
801    /// Melt quote type
802    type MeltQuote: Clone + Send + Sync;
803    /// Payment method type
804    type PaymentMethod: Clone + Send + Sync;
805    /// Melt options type
806    type MeltOptions: Clone + Send + Sync;
807    /// Operation ID type (CDK uses `Uuid`, FFI uses `String`)
808    type OperationId: Clone + Send + Sync;
809    /// Prepared send type
810    type PreparedSend<'a>: Send + Sync
811    where
812        Self: 'a;
813    /// Prepared melt type
814    type PreparedMelt<'a>: Send + Sync
815    where
816        Self: 'a;
817    /// Active subscription handle for receiving notifications
818    type Subscription: Send + Sync;
819    /// Subscribe params type
820    type SubscribeParams: Clone + Send + Sync;
821    /// Saga recovery report type
822    type RecoveryReport: Clone + Send + Sync;
823
824    /// Get the mint URL this wallet is connected to
825    fn mint_url(&self) -> Self::MintUrl;
826
827    /// Get the currency unit of this wallet
828    fn unit(&self) -> Self::CurrencyUnit;
829
830    /// Total unspent balance of the wallet
831    async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
832
833    /// Total pending balance of the wallet
834    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
835
836    /// Total reserved balance of the wallet
837    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
838
839    /// Fetch mint info from the mint (always makes a network call)
840    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
841
842    /// Load mint info (from cache if fresh, otherwise fetches)
843    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
844
845    /// Refresh keysets from the mint (always fetches fresh data)
846    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
847
848    /// Get the active keyset with lowest fees
849    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
850
851    /// Load keys for a specific keyset from cache or mint
852    async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Self::Error>;
853
854    /// Get keysets for this wallet's unit, filtered by active/all
855    async fn get_mint_keysets(
856        &self,
857        filter: KeysetFilter,
858    ) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
859
860    /// Load active keysets (alias for get_mint_keysets with Active filter)
861    async fn load_mint_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
862        self.get_mint_keysets(KeysetFilter::Active).await
863    }
864
865    /// Fetch the active keyset with lowest fees
866    async fn fetch_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
867
868    /// Get fees and available amounts for all keysets
869    async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
870
871    /// Get fee for count of proofs in a keyset
872    async fn get_keyset_count_fee(
873        &self,
874        keyset_id: &Id,
875        count: u64,
876    ) -> Result<Self::Amount, Self::Error>;
877
878    /// Get fees and amounts for a specific keyset ID
879    async fn get_keyset_fees_and_amounts_by_id(
880        &self,
881        keyset_id: Id,
882    ) -> Result<FeeAndAmounts, Self::Error>;
883
884    /// Create a mint quote for the given payment method
885    async fn mint_quote(
886        &self,
887        method: Self::PaymentMethod,
888        amount: Option<Self::Amount>,
889        description: Option<String>,
890        extra: Option<String>,
891    ) -> Result<Self::MintQuote, Self::Error>;
892
893    /// Create a melt quote for the given payment method
894    async fn melt_quote(
895        &self,
896        method: Self::PaymentMethod,
897        request: String,
898        options: Option<Self::MeltOptions>,
899        extra: Option<String>,
900    ) -> Result<Self::MeltQuote, Self::Error>;
901
902    /// List transactions, optionally filtered by direction
903    async fn list_transactions(
904        &self,
905        direction: Option<TransactionDirection>,
906    ) -> Result<Vec<Transaction>, Self::Error>;
907
908    /// Get a transaction by ID
909    async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
910
911    /// Get proofs for a transaction by transaction ID
912    async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
913
914    /// Revert a transaction by reclaiming unspent proofs
915    async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
916
917    /// Check all pending proofs and return total amount still pending
918    async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
919
920    /// Recover from incomplete operations after a crash
921    async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
922
923    /// Check if proofs are spent
924    async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
925
926    /// Get fees for a specific keyset ID
927    async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
928
929    /// Calculate fee for a given number of proofs with the specified keyset
930    async fn calculate_fee(
931        &self,
932        proof_count: u64,
933        keyset_id: Id,
934    ) -> Result<Self::Amount, Self::Error>;
935
936    /// Receive an encoded token
937    async fn receive(
938        &self,
939        encoded_token: &str,
940        options: ReceiveOptions,
941    ) -> Result<Self::Amount, Self::Error>;
942
943    /// Receive proofs directly
944    async fn receive_proofs(
945        &self,
946        proofs: Proofs,
947        options: ReceiveOptions,
948        memo: Option<String>,
949        token: Option<String>,
950    ) -> Result<Self::Amount, Self::Error>;
951
952    /// Prepare a send transaction
953    async fn prepare_send(
954        &self,
955        amount: Self::Amount,
956        options: SendOptions,
957    ) -> Result<Self::PreparedSend<'_>, Self::Error>;
958
959    /// Get pending send operation IDs
960    async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
961
962    /// Revoke a pending send operation
963    async fn revoke_send(
964        &self,
965        operation_id: Self::OperationId,
966    ) -> Result<Self::Amount, Self::Error>;
967
968    /// Check if a pending send has been claimed
969    async fn check_send_status(&self, operation_id: Self::OperationId)
970        -> Result<bool, Self::Error>;
971
972    /// Mint tokens for a quote
973    async fn mint(
974        &self,
975        quote_id: &str,
976        split_target: SplitTarget,
977        spending_conditions: Option<SpendingConditions>,
978    ) -> Result<Proofs, Self::Error>;
979
980    /// Check mint quote status
981    async fn check_mint_quote_status(&self, quote_id: &str)
982        -> Result<Self::MintQuote, Self::Error>;
983
984    /// Fetch a mint quote from the mint and store it locally
985    async fn fetch_mint_quote(
986        &self,
987        quote_id: &str,
988        payment_method: Option<Self::PaymentMethod>,
989    ) -> Result<Self::MintQuote, Self::Error>;
990
991    /// Prepare a melt operation
992    async fn prepare_melt(
993        &self,
994        quote_id: &str,
995        metadata: HashMap<String, String>,
996    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
997
998    /// Prepare a melt operation with specific proofs
999    async fn prepare_melt_proofs(
1000        &self,
1001        quote_id: &str,
1002        proofs: Proofs,
1003        metadata: HashMap<String, String>,
1004    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1005
1006    /// Prepare a melt operation from an encoded token
1007    ///
1008    /// Decodes the token, extracts proofs (handling keyset state internally),
1009    /// and prepares the melt. This is useful when the caller has a token and
1010    /// wants to skip manual decoding, which requires keyset state for v2 keysets.
1011    async fn prepare_melt_token(
1012        &self,
1013        quote_id: &str,
1014        encoded_token: &str,
1015        metadata: HashMap<String, String>,
1016    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1017
1018    /// Swap proofs
1019    async fn swap(
1020        &self,
1021        amount: Option<Self::Amount>,
1022        split_target: SplitTarget,
1023        input_proofs: Proofs,
1024        spending_conditions: Option<SpendingConditions>,
1025        include_fees: bool,
1026        use_p2bk: bool,
1027    ) -> Result<Option<Proofs>, Self::Error>;
1028
1029    /// Set Clear Auth Token (CAT)
1030    async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
1031
1032    /// Set refresh token
1033    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
1034
1035    /// Refresh access token using stored refresh token
1036    async fn refresh_access_token(&self) -> Result<(), Self::Error>;
1037
1038    /// Mint blind auth tokens
1039    async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
1040
1041    /// Get unspent auth proofs
1042    async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
1043
1044    /// Restore wallet from seed
1045    async fn restore(&self) -> Result<Restored, Self::Error>;
1046
1047    /// Restore wallet from seed with custom [`NUT13Options`]
1048    async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, Self::Error>;
1049
1050    /// Verify DLEQ proofs in a token
1051    async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
1052
1053    /// Pay a NUT-18 payment request
1054    async fn pay_request(
1055        &self,
1056        request: PaymentRequest,
1057        custom_amount: Option<Self::Amount>,
1058    ) -> Result<(), Self::Error>;
1059
1060    /// Subscribe to mint quote state updates
1061    ///
1062    /// Returns a subscription handle that receives notifications when
1063    /// any of the given mint quotes change state (e.g., Unpaid → Paid → Issued).
1064    async fn subscribe_mint_quote_state(
1065        &self,
1066        quote_ids: Vec<String>,
1067        method: Self::PaymentMethod,
1068    ) -> Result<Self::Subscription, Self::Error>;
1069
1070    /// Set metadata cache TTL (time-to-live) in seconds
1071    ///
1072    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
1073    /// before requiring a refresh from the mint server.
1074    /// If `None`, cache never expires and is always used.
1075    fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
1076
1077    /// Subscribe to wallet events
1078    async fn subscribe(
1079        &self,
1080        params: Self::SubscribeParams,
1081    ) -> Result<Self::Subscription, Self::Error>;
1082
1083    /// Get a melt quote for a BIP353 address
1084    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1085    async fn melt_bip353_quote(
1086        &self,
1087        bip353_address: &str,
1088        amount_msat: Self::Amount,
1089        network: bitcoin::Network,
1090    ) -> Result<Self::MeltQuote, Self::Error>;
1091
1092    /// Get a melt quote for a Lightning address
1093    #[cfg(not(target_arch = "wasm32"))]
1094    async fn melt_lightning_address_quote(
1095        &self,
1096        lightning_address: &str,
1097        amount_msat: Self::Amount,
1098    ) -> Result<Self::MeltQuote, Self::Error>;
1099
1100    /// Get a melt quote for a human-readable address
1101    ///
1102    /// Accepts a human-readable address that could be either a BIP353 address
1103    /// or a Lightning address. Tries BIP353 first if mint supports Bolt12,
1104    /// falls back to Lightning address.
1105    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1106    async fn melt_human_readable_quote(
1107        &self,
1108        address: &str,
1109        amount_msat: Self::Amount,
1110        network: bitcoin::Network,
1111    ) -> Result<Self::MeltQuote, Self::Error>;
1112
1113    /// Get a melt quote for a human-readable address (alias for `melt_human_readable_quote`)
1114    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1115    async fn melt_human_readable(
1116        &self,
1117        address: &str,
1118        amount_msat: Self::Amount,
1119        network: bitcoin::Network,
1120    ) -> Result<Self::MeltQuote, Self::Error> {
1121        self.melt_human_readable_quote(address, amount_msat, network)
1122            .await
1123    }
1124
1125    /// Check a mint quote status (alias for `check_mint_quote_status`)
1126    async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1127        self.check_mint_quote_status(quote_id).await
1128    }
1129
1130    /// Mint tokens for a quote (alias for `mint`)
1131    async fn mint_unified(
1132        &self,
1133        quote_id: &str,
1134        split_target: SplitTarget,
1135        spending_conditions: Option<SpendingConditions>,
1136    ) -> Result<Proofs, Self::Error> {
1137        self.mint(quote_id, split_target, spending_conditions).await
1138    }
1139
1140    /// Get proofs filtered by states
1141    ///
1142    /// Returns all proofs whose state matches any of the given states.
1143    /// The `Spent` state is typically excluded since spent proofs are removed
1144    /// from the database.
1145    async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1146
1147    // P2PK proofs
1148    /// generates and stores public key in database
1149    async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1150
1151    /// gets public key by it's hex value
1152    async fn get_public_key(
1153        &self,
1154        pubkey: &PublicKey,
1155    ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1156
1157    /// gets list of stored public keys in database
1158    async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1159
1160    /// Gets the latest generated P2PK signing key (most recently created)
1161    async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1162
1163    /// try to get secret key from p2pk signing key in localstore
1164    async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1165}
1166
1167/// Public key generated for proof signing
1168#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1169pub struct P2PKSigningKey {
1170    /// Public key
1171    pub pubkey: PublicKey,
1172    /// Derivation path
1173    pub derivation_path: DerivationPath,
1174    /// Derivation index
1175    pub derivation_index: u32,
1176    /// Created time
1177    pub created_time: u64,
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182    use super::*;
1183    use crate::nuts::Id;
1184    use crate::secret::Secret;
1185
1186    #[test]
1187    fn test_transaction_id_from_hex() {
1188        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1189        let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1190        assert_eq!(transaction_id.to_string(), hex_str);
1191    }
1192
1193    #[test]
1194    fn test_transaction_id_from_hex_empty_string() {
1195        let hex_str = "";
1196        let res = TransactionId::from_hex(hex_str);
1197        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1198    }
1199
1200    #[test]
1201    fn test_transaction_id_from_hex_longer_string() {
1202        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1203        let res = TransactionId::from_hex(hex_str);
1204        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1205    }
1206
1207    #[test]
1208    fn test_matches_conditions() {
1209        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1210        let proof = Proof::new(
1211            Amount::from(64),
1212            keyset_id,
1213            Secret::new("test_secret"),
1214            PublicKey::from_hex(
1215                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1216            )
1217            .unwrap(),
1218        );
1219
1220        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1221        let proof_info =
1222            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1223
1224        // Test matching mint_url
1225        assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1226        assert!(!proof_info.matches_conditions(
1227            &Some(MintUrl::from_str("https://different.com").unwrap()),
1228            &None,
1229            &None,
1230            &None
1231        ));
1232
1233        // Test matching unit
1234        assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1235        assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1236
1237        // Test matching state
1238        assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1239        assert!(proof_info.matches_conditions(
1240            &None,
1241            &None,
1242            &Some(vec![State::Unspent, State::Spent]),
1243            &None
1244        ));
1245        assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1246
1247        // Test with no conditions (should match)
1248        assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1249
1250        // Test with multiple conditions
1251        assert!(proof_info.matches_conditions(
1252            &Some(mint_url),
1253            &Some(CurrencyUnit::Sat),
1254            &Some(vec![State::Unspent]),
1255            &None
1256        ));
1257    }
1258
1259    #[test]
1260    fn test_matches_conditions_with_spending_conditions() {
1261        // This test would need to be expanded with actual SpendingConditions
1262        // implementation, but we can test the basic case where no spending
1263        // conditions are present
1264
1265        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1266        let proof = Proof::new(
1267            Amount::from(64),
1268            keyset_id,
1269            Secret::new("test_secret"),
1270            PublicKey::from_hex(
1271                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1272            )
1273            .unwrap(),
1274        );
1275
1276        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1277        let proof_info =
1278            ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1279
1280        // Test with empty spending conditions (should match when proof has none)
1281        assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1282
1283        // Test with non-empty spending conditions (should not match when proof has none)
1284        let dummy_condition = SpendingConditions::P2PKConditions {
1285            data: SecretKey::generate().public_key(),
1286            conditions: None,
1287        };
1288        assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1289    }
1290
1291    #[test]
1292    fn test_wallet_options_debug_redacts_p2pk_signing_keys() {
1293        let secret_key = SecretKey::generate();
1294        let secret_hex = secret_key.to_secret_hex();
1295
1296        let send_options = SendOptions {
1297            p2pk_signing_keys: vec![secret_key.clone()],
1298            ..Default::default()
1299        };
1300        let receive_options = ReceiveOptions {
1301            p2pk_signing_keys: vec![secret_key],
1302            ..Default::default()
1303        };
1304
1305        let send_debug = format!("{:?}", send_options);
1306        let receive_debug = format!("{:?}", receive_options);
1307
1308        assert!(!send_debug.contains(&secret_hex));
1309        assert!(send_debug.contains("[redacted]"));
1310        assert!(!receive_debug.contains(&secret_hex));
1311        assert!(receive_debug.contains("[redacted]"));
1312    }
1313
1314    #[test]
1315    fn nut13_options_defaults_match_nut13_spec() {
1316        // NUT-13 recommends batch_size=100 and gap_limit=3.
1317        // https://github.com/cashubtc/nuts/blob/main/13.md#generate-blindedmessages
1318        let opts = NUT13Options::default();
1319        assert_eq!(opts.batch_size, NUT13Options::DEFAULT_BATCH_SIZE);
1320        assert_eq!(opts.max_gap, NUT13Options::DEFAULT_MAX_GAP);
1321    }
1322
1323    #[test]
1324    fn nut13_options_new_accepts_custom_values() {
1325        let opts = NUT13Options::new(25, 2).unwrap();
1326        let cloned = opts.clone();
1327        assert_eq!(cloned.batch_size, 25);
1328        assert_eq!(cloned.max_gap, 2);
1329    }
1330
1331    #[test]
1332    fn nut13_options_reject_zero_batch_size() {
1333        let err = NUT13Options::new(0, 2).unwrap_err();
1334        assert!(matches!(
1335            err,
1336            Error::InvalidNut13Options {
1337                field: "batch_size",
1338                ..
1339            }
1340        ));
1341    }
1342
1343    #[test]
1344    fn nut13_options_reject_zero_max_gap() {
1345        let err = NUT13Options::new(25, 0).unwrap_err();
1346        assert!(matches!(
1347            err,
1348            Error::InvalidNut13Options {
1349                field: "max_gap",
1350                ..
1351            }
1352        ));
1353    }
1354}