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