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::SplitTarget;
11use cashu::nuts::nut07::ProofState;
12use cashu::nuts::nut18::PaymentRequest;
13use cashu::nuts::AuthProof;
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/// Unified wallet trait providing a common interface for wallet operations.
638///
639/// This trait abstracts over different wallet implementations (CDK wallet, FFI
640/// wrappers, etc.) and provides a consistent interface for balance queries,
641/// minting, melting, keyset management, and other core wallet operations.
642///
643/// All domain types are associated types so each implementation can use its own
644/// type system (e.g. FFI-friendly records vs native Rust types).
645#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
646#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
647pub trait Wallet: Send + Sync {
648    /// Error type
649    type Error: std::error::Error + Send + Sync + 'static;
650    /// Amount type (e.g. `cdk_common::Amount` or FFI `Amount`)
651    type Amount: Clone + Send + Sync;
652    /// Mint URL type
653    type MintUrl: Clone + Send + Sync;
654    /// Currency unit type
655    type CurrencyUnit: Clone + Send + Sync;
656    /// Mint info type
657    type MintInfo: Clone + Send + Sync;
658    /// Keyset info type
659    type KeySetInfo: Clone + Send + Sync;
660    /// Mint quote type
661    type MintQuote: Clone + Send + Sync;
662    /// Melt quote type
663    type MeltQuote: Clone + Send + Sync;
664    /// Payment method type
665    type PaymentMethod: Clone + Send + Sync;
666    /// Melt options type
667    type MeltOptions: Clone + Send + Sync;
668    /// Operation ID type (CDK uses `Uuid`, FFI uses `String`)
669    type OperationId: Clone + Send + Sync;
670    /// Prepared send type
671    type PreparedSend<'a>: Send + Sync
672    where
673        Self: 'a;
674    /// Prepared melt type
675    type PreparedMelt<'a>: Send + Sync
676    where
677        Self: 'a;
678    /// Active subscription handle for receiving notifications
679    type Subscription: Send + Sync;
680    /// Subscribe params type
681    type SubscribeParams: Clone + Send + Sync;
682
683    /// Get the mint URL this wallet is connected to
684    fn mint_url(&self) -> Self::MintUrl;
685
686    /// Get the currency unit of this wallet
687    fn unit(&self) -> Self::CurrencyUnit;
688
689    /// Total unspent balance of the wallet
690    async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
691
692    /// Total pending balance of the wallet
693    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
694
695    /// Total reserved balance of the wallet
696    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
697
698    /// Fetch mint info from the mint (always makes a network call)
699    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
700
701    /// Load mint info (from cache if fresh, otherwise fetches)
702    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
703
704    /// Refresh keysets from the mint (always fetches fresh data)
705    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
706
707    /// Get the active keyset with lowest fees
708    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
709
710    /// Create a mint quote for the given payment method
711    async fn mint_quote(
712        &self,
713        method: Self::PaymentMethod,
714        amount: Option<Self::Amount>,
715        description: Option<String>,
716        extra: Option<String>,
717    ) -> Result<Self::MintQuote, Self::Error>;
718
719    /// Create a melt quote for the given payment method
720    async fn melt_quote(
721        &self,
722        method: Self::PaymentMethod,
723        request: String,
724        options: Option<Self::MeltOptions>,
725        extra: Option<String>,
726    ) -> Result<Self::MeltQuote, Self::Error>;
727
728    /// List transactions, optionally filtered by direction
729    async fn list_transactions(
730        &self,
731        direction: Option<TransactionDirection>,
732    ) -> Result<Vec<Transaction>, Self::Error>;
733
734    /// Get a transaction by ID
735    async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
736
737    /// Get proofs for a transaction by transaction ID
738    async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
739
740    /// Revert a transaction by reclaiming unspent proofs
741    async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
742
743    /// Check all pending proofs and return total amount still pending
744    async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
745
746    /// Check if proofs are spent
747    async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
748
749    /// Get fees for a specific keyset ID
750    async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
751
752    /// Calculate fee for a given number of proofs with the specified keyset
753    async fn calculate_fee(
754        &self,
755        proof_count: u64,
756        keyset_id: Id,
757    ) -> Result<Self::Amount, Self::Error>;
758
759    /// Receive an encoded token
760    async fn receive(
761        &self,
762        encoded_token: &str,
763        options: ReceiveOptions,
764    ) -> Result<Self::Amount, Self::Error>;
765
766    /// Receive proofs directly
767    async fn receive_proofs(
768        &self,
769        proofs: Proofs,
770        options: ReceiveOptions,
771        memo: Option<String>,
772        token: Option<String>,
773    ) -> Result<Self::Amount, Self::Error>;
774
775    /// Prepare a send transaction
776    async fn prepare_send(
777        &self,
778        amount: Self::Amount,
779        options: SendOptions,
780    ) -> Result<Self::PreparedSend<'_>, Self::Error>;
781
782    /// Get pending send operation IDs
783    async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
784
785    /// Revoke a pending send operation
786    async fn revoke_send(
787        &self,
788        operation_id: Self::OperationId,
789    ) -> Result<Self::Amount, Self::Error>;
790
791    /// Check if a pending send has been claimed
792    async fn check_send_status(&self, operation_id: Self::OperationId)
793        -> Result<bool, Self::Error>;
794
795    /// Mint tokens for a quote
796    async fn mint(
797        &self,
798        quote_id: &str,
799        split_target: SplitTarget,
800        spending_conditions: Option<SpendingConditions>,
801    ) -> Result<Proofs, Self::Error>;
802
803    /// Check mint quote status
804    async fn check_mint_quote_status(&self, quote_id: &str)
805        -> Result<Self::MintQuote, Self::Error>;
806
807    /// Fetch a mint quote from the mint and store it locally
808    async fn fetch_mint_quote(
809        &self,
810        quote_id: &str,
811        payment_method: Option<Self::PaymentMethod>,
812    ) -> Result<Self::MintQuote, Self::Error>;
813
814    /// Prepare a melt operation
815    async fn prepare_melt(
816        &self,
817        quote_id: &str,
818        metadata: HashMap<String, String>,
819    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
820
821    /// Prepare a melt operation with specific proofs
822    async fn prepare_melt_proofs(
823        &self,
824        quote_id: &str,
825        proofs: Proofs,
826        metadata: HashMap<String, String>,
827    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
828
829    /// Swap proofs
830    async fn swap(
831        &self,
832        amount: Option<Self::Amount>,
833        split_target: SplitTarget,
834        input_proofs: Proofs,
835        spending_conditions: Option<SpendingConditions>,
836        include_fees: bool,
837        use_p2bk: bool,
838    ) -> Result<Option<Proofs>, Self::Error>;
839
840    /// Set Clear Auth Token (CAT)
841    async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
842
843    /// Set refresh token
844    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
845
846    /// Refresh access token using stored refresh token
847    async fn refresh_access_token(&self) -> Result<(), Self::Error>;
848
849    /// Mint blind auth tokens
850    async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
851
852    /// Get unspent auth proofs
853    async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
854
855    /// Restore wallet from seed
856    async fn restore(&self) -> Result<Restored, Self::Error>;
857
858    /// Verify DLEQ proofs in a token
859    async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
860
861    /// Pay a NUT-18 payment request
862    async fn pay_request(
863        &self,
864        request: PaymentRequest,
865        custom_amount: Option<Self::Amount>,
866    ) -> Result<(), Self::Error>;
867
868    /// Subscribe to mint quote state updates
869    ///
870    /// Returns a subscription handle that receives notifications when
871    /// any of the given mint quotes change state (e.g., Unpaid → Paid → Issued).
872    async fn subscribe_mint_quote_state(
873        &self,
874        quote_ids: Vec<String>,
875        method: Self::PaymentMethod,
876    ) -> Result<Self::Subscription, Self::Error>;
877
878    /// Set metadata cache TTL (time-to-live) in seconds
879    ///
880    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
881    /// before requiring a refresh from the mint server.
882    /// If `None`, cache never expires and is always used.
883    fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
884
885    /// Subscribe to wallet events
886    async fn subscribe(
887        &self,
888        params: Self::SubscribeParams,
889    ) -> Result<Self::Subscription, Self::Error>;
890
891    /// Get a melt quote for a BIP353 address
892    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
893    async fn melt_bip353_quote(
894        &self,
895        bip353_address: &str,
896        amount_msat: Self::Amount,
897        network: bitcoin::Network,
898    ) -> Result<Self::MeltQuote, Self::Error>;
899
900    /// Get a melt quote for a Lightning address
901    #[cfg(not(target_arch = "wasm32"))]
902    async fn melt_lightning_address_quote(
903        &self,
904        lightning_address: &str,
905        amount_msat: Self::Amount,
906    ) -> Result<Self::MeltQuote, Self::Error>;
907
908    /// Get a melt quote for a human-readable address
909    ///
910    /// Accepts a human-readable address that could be either a BIP353 address
911    /// or a Lightning address. Tries BIP353 first if mint supports Bolt12,
912    /// falls back to Lightning address.
913    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
914    async fn melt_human_readable_quote(
915        &self,
916        address: &str,
917        amount_msat: Self::Amount,
918        network: bitcoin::Network,
919    ) -> Result<Self::MeltQuote, Self::Error>;
920
921    /// Get a melt quote for a human-readable address (alias for `melt_human_readable_quote`)
922    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
923    async fn melt_human_readable(
924        &self,
925        address: &str,
926        amount_msat: Self::Amount,
927        network: bitcoin::Network,
928    ) -> Result<Self::MeltQuote, Self::Error> {
929        self.melt_human_readable_quote(address, amount_msat, network)
930            .await
931    }
932
933    /// Check a mint quote status (alias for `check_mint_quote_status`)
934    async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
935        self.check_mint_quote_status(quote_id).await
936    }
937
938    /// Mint tokens for a quote (alias for `mint`)
939    async fn mint_unified(
940        &self,
941        quote_id: &str,
942        split_target: SplitTarget,
943        spending_conditions: Option<SpendingConditions>,
944    ) -> Result<Proofs, Self::Error> {
945        self.mint(quote_id, split_target, spending_conditions).await
946    }
947
948    /// Get proofs filtered by states
949    ///
950    /// Returns all proofs whose state matches any of the given states.
951    /// The `Spent` state is typically excluded since spent proofs are removed
952    /// from the database.
953    async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
954
955    // P2PK proofs
956    /// generates and stores public key in database
957    async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
958
959    /// gets public key by it's hex value
960    async fn get_public_key(
961        &self,
962        pubkey: &PublicKey,
963    ) -> Result<Option<P2PKSigningKey>, Self::Error>;
964
965    /// gets list of stored public keys in database
966    async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
967
968    /// Gets the latest generated P2PK signing key (most recently created)
969    async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
970
971    /// try to get secret key from p2pk signing key in localstore
972    async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
973}
974
975/// Public key generated for proof signing
976#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
977pub struct P2PKSigningKey {
978    /// Public key
979    pub pubkey: PublicKey,
980    /// Derivation path
981    pub derivation_path: DerivationPath,
982    /// Derivation index
983    pub derivation_index: u32,
984    /// Created time
985    pub created_time: u64,
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991    use crate::nuts::Id;
992    use crate::secret::Secret;
993
994    #[test]
995    fn test_transaction_id_from_hex() {
996        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
997        let transaction_id = TransactionId::from_hex(hex_str).unwrap();
998        assert_eq!(transaction_id.to_string(), hex_str);
999    }
1000
1001    #[test]
1002    fn test_transaction_id_from_hex_empty_string() {
1003        let hex_str = "";
1004        let res = TransactionId::from_hex(hex_str);
1005        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1006    }
1007
1008    #[test]
1009    fn test_transaction_id_from_hex_longer_string() {
1010        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1011        let res = TransactionId::from_hex(hex_str);
1012        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1013    }
1014
1015    #[test]
1016    fn test_matches_conditions() {
1017        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1018        let proof = Proof::new(
1019            Amount::from(64),
1020            keyset_id,
1021            Secret::new("test_secret"),
1022            PublicKey::from_hex(
1023                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1024            )
1025            .unwrap(),
1026        );
1027
1028        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1029        let proof_info =
1030            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1031
1032        // Test matching mint_url
1033        assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1034        assert!(!proof_info.matches_conditions(
1035            &Some(MintUrl::from_str("https://different.com").unwrap()),
1036            &None,
1037            &None,
1038            &None
1039        ));
1040
1041        // Test matching unit
1042        assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1043        assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1044
1045        // Test matching state
1046        assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1047        assert!(proof_info.matches_conditions(
1048            &None,
1049            &None,
1050            &Some(vec![State::Unspent, State::Spent]),
1051            &None
1052        ));
1053        assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1054
1055        // Test with no conditions (should match)
1056        assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1057
1058        // Test with multiple conditions
1059        assert!(proof_info.matches_conditions(
1060            &Some(mint_url),
1061            &Some(CurrencyUnit::Sat),
1062            &Some(vec![State::Unspent]),
1063            &None
1064        ));
1065    }
1066
1067    #[test]
1068    fn test_matches_conditions_with_spending_conditions() {
1069        // This test would need to be expanded with actual SpendingConditions
1070        // implementation, but we can test the basic case where no spending
1071        // conditions are present
1072
1073        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1074        let proof = Proof::new(
1075            Amount::from(64),
1076            keyset_id,
1077            Secret::new("test_secret"),
1078            PublicKey::from_hex(
1079                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1080            )
1081            .unwrap(),
1082        );
1083
1084        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1085        let proof_info =
1086            ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1087
1088        // Test with empty spending conditions (should match when proof has none)
1089        assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1090
1091        // Test with non-empty spending conditions (should not match when proof has none)
1092        let dummy_condition = SpendingConditions::P2PKConditions {
1093            data: SecretKey::generate().public_key(),
1094            conditions: None,
1095        };
1096        assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1097    }
1098}