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/// Send options
332#[derive(Debug, Clone, Default)]
333pub struct SendOptions {
334    /// Memo
335    pub memo: Option<SendMemo>,
336    /// Spending conditions
337    pub conditions: Option<SpendingConditions>,
338    /// Amount split target
339    pub amount_split_target: SplitTarget,
340    /// Send kind
341    pub send_kind: SendKind,
342    /// Include fee
343    pub include_fee: bool,
344    /// Maximum number of proofs to include in the token
345    pub max_proofs: Option<usize>,
346    /// Metadata
347    pub metadata: HashMap<String, String>,
348    /// Use P2BK (NUT-28)
349    pub use_p2bk: bool,
350    /// Signing keys for P2PK-locked input proofs; auto-detected from the wallet keyring if omitted
351    pub p2pk_signing_keys: Vec<SecretKey>,
352    /// How P2PK-locked input proofs should be handled during send
353    pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
354}
355
356/// Send behavior for selected P2PK-locked input proofs
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
358pub enum P2PKLockedProofSendMode {
359    /// Swap locked proofs into fresh proofs before creating the token
360    #[default]
361    Swap,
362    /// Sign locked proofs and include them directly in the token
363    SignAndSend,
364}
365
366/// Send memo
367#[derive(Debug, Clone)]
368pub struct SendMemo {
369    /// Memo
370    pub memo: String,
371    /// Include memo in token
372    pub include_memo: bool,
373}
374
375impl SendMemo {
376    /// Create a new send memo
377    pub fn for_token(memo: &str) -> Self {
378        Self {
379            memo: memo.to_string(),
380            include_memo: true,
381        }
382    }
383}
384
385/// Receive options
386#[derive(Debug, Clone, Default)]
387pub struct ReceiveOptions {
388    /// Amount split target
389    pub amount_split_target: SplitTarget,
390    /// P2PK signing keys
391    pub p2pk_signing_keys: Vec<SecretKey>,
392    /// Preimages
393    pub preimages: Vec<String>,
394    /// Metadata
395    pub metadata: HashMap<String, String>,
396}
397
398/// Send Kind
399#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
400pub enum SendKind {
401    #[default]
402    /// Allow online swap before send if wallet does not have exact amount
403    OnlineExact,
404    /// Prefer offline send if difference is less then tolerance
405    OnlineTolerance(Amount),
406    /// Wallet cannot do an online swap and selected proof must be exactly send amount
407    OfflineExact,
408    /// Wallet must remain offline but can over pay if below tolerance
409    OfflineTolerance(Amount),
410}
411
412impl SendKind {
413    /// Check if send kind is online
414    pub fn is_online(&self) -> bool {
415        matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
416    }
417
418    /// Check if send kind is offline
419    pub fn is_offline(&self) -> bool {
420        matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
421    }
422
423    /// Check if send kind is exact
424    pub fn is_exact(&self) -> bool {
425        matches!(self, Self::OnlineExact | Self::OfflineExact)
426    }
427
428    /// Check if send kind has tolerance
429    pub fn has_tolerance(&self) -> bool {
430        matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
431    }
432}
433
434/// Wallet Transaction
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
436pub struct Transaction {
437    /// Mint Url
438    pub mint_url: MintUrl,
439    /// Transaction direction
440    pub direction: TransactionDirection,
441    /// Amount
442    pub amount: Amount,
443    /// Fee
444    pub fee: Amount,
445    /// Currency Unit
446    pub unit: CurrencyUnit,
447    /// Proof Ys
448    pub ys: Vec<PublicKey>,
449    /// Unix timestamp
450    pub timestamp: u64,
451    /// Memo
452    pub memo: Option<String>,
453    /// User-defined metadata
454    pub metadata: HashMap<String, String>,
455    /// Quote ID if this is a mint or melt transaction
456    pub quote_id: Option<String>,
457    /// Payment request (e.g., BOLT11 invoice, BOLT12 offer)
458    pub payment_request: Option<String>,
459    /// Payment proof (e.g., preimage for Lightning melt transactions)
460    #[serde(alias = "payment_preimage")]
461    pub payment_proof: Option<String>,
462    /// Payment method (e.g., Bolt11, Bolt12) for mint/melt transactions
463    #[serde(default)]
464    pub payment_method: Option<PaymentMethod>,
465    /// Saga ID if this transaction was part of a saga
466    #[serde(default)]
467    pub saga_id: Option<Uuid>,
468}
469
470impl Transaction {
471    /// Transaction ID
472    pub fn id(&self) -> TransactionId {
473        TransactionId::new(self.ys.clone())
474    }
475
476    /// Check if transaction matches conditions
477    pub fn matches_conditions(
478        &self,
479        mint_url: &Option<MintUrl>,
480        direction: &Option<TransactionDirection>,
481        unit: &Option<CurrencyUnit>,
482    ) -> bool {
483        if let Some(mint_url) = mint_url {
484            if &self.mint_url != mint_url {
485                return false;
486            }
487        }
488        if let Some(direction) = direction {
489            if &self.direction != direction {
490                return false;
491            }
492        }
493        if let Some(unit) = unit {
494            if &self.unit != unit {
495                return false;
496            }
497        }
498        true
499    }
500}
501
502impl PartialOrd for Transaction {
503    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
504        Some(self.cmp(other))
505    }
506}
507
508impl Ord for Transaction {
509    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
510        self.timestamp
511            .cmp(&other.timestamp)
512            .reverse()
513            .then_with(|| self.id().cmp(&other.id()))
514    }
515}
516
517/// Transaction Direction
518#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub enum TransactionDirection {
520    /// Incoming transaction (i.e., receive or mint)
521    Incoming,
522    /// Outgoing transaction (i.e., send or melt)
523    Outgoing,
524}
525
526impl std::fmt::Display for TransactionDirection {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        match self {
529            TransactionDirection::Incoming => write!(f, "Incoming"),
530            TransactionDirection::Outgoing => write!(f, "Outgoing"),
531        }
532    }
533}
534
535impl FromStr for TransactionDirection {
536    type Err = Error;
537
538    fn from_str(value: &str) -> Result<Self, Self::Err> {
539        match value {
540            "Incoming" => Ok(Self::Incoming),
541            "Outgoing" => Ok(Self::Outgoing),
542            _ => Err(Error::InvalidTransactionDirection),
543        }
544    }
545}
546
547/// Transaction ID
548#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
549#[serde(transparent)]
550pub struct TransactionId([u8; 32]);
551
552impl TransactionId {
553    /// Create new [`TransactionId`]
554    pub fn new(ys: Vec<PublicKey>) -> Self {
555        let mut ys = ys;
556        ys.sort();
557        let mut hasher = sha256::Hash::engine();
558        for y in ys {
559            hasher.input(&y.to_bytes());
560        }
561        let hash = sha256::Hash::from_engine(hasher);
562        Self(hash.to_byte_array())
563    }
564
565    /// From proofs
566    pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
567        let ys = proofs
568            .iter()
569            .map(|proof| proof.y())
570            .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
571        Ok(Self::new(ys))
572    }
573
574    /// From bytes
575    pub fn from_bytes(bytes: [u8; 32]) -> Self {
576        Self(bytes)
577    }
578
579    /// From hex string
580    pub fn from_hex(value: &str) -> Result<Self, Error> {
581        let bytes = hex::decode(value)?;
582        if bytes.len() != 32 {
583            return Err(Error::InvalidTransactionId);
584        }
585        let mut array = [0u8; 32];
586        array.copy_from_slice(&bytes);
587        Ok(Self(array))
588    }
589
590    /// From slice
591    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
592        if slice.len() != 32 {
593            return Err(Error::InvalidTransactionId);
594        }
595        let mut array = [0u8; 32];
596        array.copy_from_slice(slice);
597        Ok(Self(array))
598    }
599
600    /// Get inner value
601    pub fn as_bytes(&self) -> &[u8; 32] {
602        &self.0
603    }
604
605    /// Get inner value as slice
606    pub fn as_slice(&self) -> &[u8] {
607        &self.0
608    }
609}
610
611impl std::fmt::Display for TransactionId {
612    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613        write!(f, "{}", hex::encode(self.0))
614    }
615}
616
617impl FromStr for TransactionId {
618    type Err = Error;
619
620    fn from_str(value: &str) -> Result<Self, Self::Err> {
621        Self::from_hex(value)
622    }
623}
624
625impl TryFrom<Proofs> for TransactionId {
626    type Error = nut00::Error;
627
628    fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
629        Self::from_proofs(proofs)
630    }
631}
632
633/// Wallet operation kind
634#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(rename_all = "snake_case")]
636pub enum OperationKind {
637    /// Send operation
638    Send,
639    /// Receive operation
640    Receive,
641    /// Swap operation
642    Swap,
643    /// Mint operation
644    Mint,
645    /// Melt operation
646    Melt,
647}
648
649impl fmt::Display for OperationKind {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        match self {
652            OperationKind::Send => write!(f, "send"),
653            OperationKind::Receive => write!(f, "receive"),
654            OperationKind::Swap => write!(f, "swap"),
655            OperationKind::Mint => write!(f, "mint"),
656            OperationKind::Melt => write!(f, "melt"),
657        }
658    }
659}
660
661impl FromStr for OperationKind {
662    type Err = Error;
663
664    fn from_str(s: &str) -> Result<Self, Self::Err> {
665        match s {
666            "send" => Ok(OperationKind::Send),
667            "receive" => Ok(OperationKind::Receive),
668            "swap" => Ok(OperationKind::Swap),
669            "mint" => Ok(OperationKind::Mint),
670            "melt" => Ok(OperationKind::Melt),
671            _ => Err(Error::InvalidOperationKind),
672        }
673    }
674}
675
676/// Filter for keyset queries
677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub enum KeysetFilter {
679    /// Only return active keysets
680    Active,
681    /// Return all keysets (active and inactive)
682    All,
683}
684
685/// Unified wallet trait providing a common interface for wallet operations.
686///
687/// This trait abstracts over different wallet implementations (CDK wallet, FFI
688/// wrappers, etc.) and provides a consistent interface for balance queries,
689/// minting, melting, keyset management, and other core wallet operations.
690///
691/// All domain types are associated types so each implementation can use its own
692/// type system (e.g. FFI-friendly records vs native Rust types).
693#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
694#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
695pub trait Wallet: Send + Sync {
696    /// Error type
697    type Error: std::error::Error + Send + Sync + 'static;
698    /// Amount type (e.g. `cdk_common::Amount` or FFI `Amount`)
699    type Amount: Clone + Send + Sync;
700    /// Mint URL type
701    type MintUrl: Clone + Send + Sync;
702    /// Currency unit type
703    type CurrencyUnit: Clone + Send + Sync;
704    /// Mint info type
705    type MintInfo: Clone + Send + Sync;
706    /// Keyset info type
707    type KeySetInfo: Clone + Send + Sync;
708    /// Mint quote type
709    type MintQuote: Clone + Send + Sync;
710    /// Melt quote type
711    type MeltQuote: Clone + Send + Sync;
712    /// Payment method type
713    type PaymentMethod: Clone + Send + Sync;
714    /// Melt options type
715    type MeltOptions: Clone + Send + Sync;
716    /// Operation ID type (CDK uses `Uuid`, FFI uses `String`)
717    type OperationId: Clone + Send + Sync;
718    /// Prepared send type
719    type PreparedSend<'a>: Send + Sync
720    where
721        Self: 'a;
722    /// Prepared melt type
723    type PreparedMelt<'a>: Send + Sync
724    where
725        Self: 'a;
726    /// Active subscription handle for receiving notifications
727    type Subscription: Send + Sync;
728    /// Subscribe params type
729    type SubscribeParams: Clone + Send + Sync;
730    /// Saga recovery report type
731    type RecoveryReport: Clone + Send + Sync;
732
733    /// Get the mint URL this wallet is connected to
734    fn mint_url(&self) -> Self::MintUrl;
735
736    /// Get the currency unit of this wallet
737    fn unit(&self) -> Self::CurrencyUnit;
738
739    /// Total unspent balance of the wallet
740    async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
741
742    /// Total pending balance of the wallet
743    async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
744
745    /// Total reserved balance of the wallet
746    async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
747
748    /// Fetch mint info from the mint (always makes a network call)
749    async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
750
751    /// Load mint info (from cache if fresh, otherwise fetches)
752    async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
753
754    /// Refresh keysets from the mint (always fetches fresh data)
755    async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
756
757    /// Get the active keyset with lowest fees
758    async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
759
760    /// Load keys for a specific keyset from cache or mint
761    async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Self::Error>;
762
763    /// Get keysets for this wallet's unit, filtered by active/all
764    async fn get_mint_keysets(
765        &self,
766        filter: KeysetFilter,
767    ) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
768
769    /// Load active keysets (alias for get_mint_keysets with Active filter)
770    async fn load_mint_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
771        self.get_mint_keysets(KeysetFilter::Active).await
772    }
773
774    /// Fetch the active keyset with lowest fees
775    async fn fetch_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
776
777    /// Get fees and available amounts for all keysets
778    async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
779
780    /// Get fee for count of proofs in a keyset
781    async fn get_keyset_count_fee(
782        &self,
783        keyset_id: &Id,
784        count: u64,
785    ) -> Result<Self::Amount, Self::Error>;
786
787    /// Get fees and amounts for a specific keyset ID
788    async fn get_keyset_fees_and_amounts_by_id(
789        &self,
790        keyset_id: Id,
791    ) -> Result<FeeAndAmounts, Self::Error>;
792
793    /// Create a mint quote for the given payment method
794    async fn mint_quote(
795        &self,
796        method: Self::PaymentMethod,
797        amount: Option<Self::Amount>,
798        description: Option<String>,
799        extra: Option<String>,
800    ) -> Result<Self::MintQuote, Self::Error>;
801
802    /// Create a melt quote for the given payment method
803    async fn melt_quote(
804        &self,
805        method: Self::PaymentMethod,
806        request: String,
807        options: Option<Self::MeltOptions>,
808        extra: Option<String>,
809    ) -> Result<Self::MeltQuote, Self::Error>;
810
811    /// List transactions, optionally filtered by direction
812    async fn list_transactions(
813        &self,
814        direction: Option<TransactionDirection>,
815    ) -> Result<Vec<Transaction>, Self::Error>;
816
817    /// Get a transaction by ID
818    async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
819
820    /// Get proofs for a transaction by transaction ID
821    async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
822
823    /// Revert a transaction by reclaiming unspent proofs
824    async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
825
826    /// Check all pending proofs and return total amount still pending
827    async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
828
829    /// Recover from incomplete operations after a crash
830    async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
831
832    /// Check if proofs are spent
833    async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
834
835    /// Get fees for a specific keyset ID
836    async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
837
838    /// Calculate fee for a given number of proofs with the specified keyset
839    async fn calculate_fee(
840        &self,
841        proof_count: u64,
842        keyset_id: Id,
843    ) -> Result<Self::Amount, Self::Error>;
844
845    /// Receive an encoded token
846    async fn receive(
847        &self,
848        encoded_token: &str,
849        options: ReceiveOptions,
850    ) -> Result<Self::Amount, Self::Error>;
851
852    /// Receive proofs directly
853    async fn receive_proofs(
854        &self,
855        proofs: Proofs,
856        options: ReceiveOptions,
857        memo: Option<String>,
858        token: Option<String>,
859    ) -> Result<Self::Amount, Self::Error>;
860
861    /// Prepare a send transaction
862    async fn prepare_send(
863        &self,
864        amount: Self::Amount,
865        options: SendOptions,
866    ) -> Result<Self::PreparedSend<'_>, Self::Error>;
867
868    /// Get pending send operation IDs
869    async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
870
871    /// Revoke a pending send operation
872    async fn revoke_send(
873        &self,
874        operation_id: Self::OperationId,
875    ) -> Result<Self::Amount, Self::Error>;
876
877    /// Check if a pending send has been claimed
878    async fn check_send_status(&self, operation_id: Self::OperationId)
879        -> Result<bool, Self::Error>;
880
881    /// Mint tokens for a quote
882    async fn mint(
883        &self,
884        quote_id: &str,
885        split_target: SplitTarget,
886        spending_conditions: Option<SpendingConditions>,
887    ) -> Result<Proofs, Self::Error>;
888
889    /// Check mint quote status
890    async fn check_mint_quote_status(&self, quote_id: &str)
891        -> Result<Self::MintQuote, Self::Error>;
892
893    /// Fetch a mint quote from the mint and store it locally
894    async fn fetch_mint_quote(
895        &self,
896        quote_id: &str,
897        payment_method: Option<Self::PaymentMethod>,
898    ) -> Result<Self::MintQuote, Self::Error>;
899
900    /// Prepare a melt operation
901    async fn prepare_melt(
902        &self,
903        quote_id: &str,
904        metadata: HashMap<String, String>,
905    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
906
907    /// Prepare a melt operation with specific proofs
908    async fn prepare_melt_proofs(
909        &self,
910        quote_id: &str,
911        proofs: Proofs,
912        metadata: HashMap<String, String>,
913    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
914
915    /// Prepare a melt operation from an encoded token
916    ///
917    /// Decodes the token, extracts proofs (handling keyset state internally),
918    /// and prepares the melt. This is useful when the caller has a token and
919    /// wants to skip manual decoding, which requires keyset state for v2 keysets.
920    async fn prepare_melt_token(
921        &self,
922        quote_id: &str,
923        encoded_token: &str,
924        metadata: HashMap<String, String>,
925    ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
926
927    /// Swap proofs
928    async fn swap(
929        &self,
930        amount: Option<Self::Amount>,
931        split_target: SplitTarget,
932        input_proofs: Proofs,
933        spending_conditions: Option<SpendingConditions>,
934        include_fees: bool,
935        use_p2bk: bool,
936    ) -> Result<Option<Proofs>, Self::Error>;
937
938    /// Set Clear Auth Token (CAT)
939    async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
940
941    /// Set refresh token
942    async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
943
944    /// Refresh access token using stored refresh token
945    async fn refresh_access_token(&self) -> Result<(), Self::Error>;
946
947    /// Mint blind auth tokens
948    async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
949
950    /// Get unspent auth proofs
951    async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
952
953    /// Restore wallet from seed
954    async fn restore(&self) -> Result<Restored, Self::Error>;
955
956    /// Verify DLEQ proofs in a token
957    async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
958
959    /// Pay a NUT-18 payment request
960    async fn pay_request(
961        &self,
962        request: PaymentRequest,
963        custom_amount: Option<Self::Amount>,
964    ) -> Result<(), Self::Error>;
965
966    /// Subscribe to mint quote state updates
967    ///
968    /// Returns a subscription handle that receives notifications when
969    /// any of the given mint quotes change state (e.g., Unpaid → Paid → Issued).
970    async fn subscribe_mint_quote_state(
971        &self,
972        quote_ids: Vec<String>,
973        method: Self::PaymentMethod,
974    ) -> Result<Self::Subscription, Self::Error>;
975
976    /// Set metadata cache TTL (time-to-live) in seconds
977    ///
978    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
979    /// before requiring a refresh from the mint server.
980    /// If `None`, cache never expires and is always used.
981    fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
982
983    /// Subscribe to wallet events
984    async fn subscribe(
985        &self,
986        params: Self::SubscribeParams,
987    ) -> Result<Self::Subscription, Self::Error>;
988
989    /// Get a melt quote for a BIP353 address
990    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
991    async fn melt_bip353_quote(
992        &self,
993        bip353_address: &str,
994        amount_msat: Self::Amount,
995        network: bitcoin::Network,
996    ) -> Result<Self::MeltQuote, Self::Error>;
997
998    /// Get a melt quote for a Lightning address
999    #[cfg(not(target_arch = "wasm32"))]
1000    async fn melt_lightning_address_quote(
1001        &self,
1002        lightning_address: &str,
1003        amount_msat: Self::Amount,
1004    ) -> Result<Self::MeltQuote, Self::Error>;
1005
1006    /// Get a melt quote for a human-readable address
1007    ///
1008    /// Accepts a human-readable address that could be either a BIP353 address
1009    /// or a Lightning address. Tries BIP353 first if mint supports Bolt12,
1010    /// falls back to Lightning address.
1011    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1012    async fn melt_human_readable_quote(
1013        &self,
1014        address: &str,
1015        amount_msat: Self::Amount,
1016        network: bitcoin::Network,
1017    ) -> Result<Self::MeltQuote, Self::Error>;
1018
1019    /// Get a melt quote for a human-readable address (alias for `melt_human_readable_quote`)
1020    #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1021    async fn melt_human_readable(
1022        &self,
1023        address: &str,
1024        amount_msat: Self::Amount,
1025        network: bitcoin::Network,
1026    ) -> Result<Self::MeltQuote, Self::Error> {
1027        self.melt_human_readable_quote(address, amount_msat, network)
1028            .await
1029    }
1030
1031    /// Check a mint quote status (alias for `check_mint_quote_status`)
1032    async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1033        self.check_mint_quote_status(quote_id).await
1034    }
1035
1036    /// Mint tokens for a quote (alias for `mint`)
1037    async fn mint_unified(
1038        &self,
1039        quote_id: &str,
1040        split_target: SplitTarget,
1041        spending_conditions: Option<SpendingConditions>,
1042    ) -> Result<Proofs, Self::Error> {
1043        self.mint(quote_id, split_target, spending_conditions).await
1044    }
1045
1046    /// Get proofs filtered by states
1047    ///
1048    /// Returns all proofs whose state matches any of the given states.
1049    /// The `Spent` state is typically excluded since spent proofs are removed
1050    /// from the database.
1051    async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1052
1053    // P2PK proofs
1054    /// generates and stores public key in database
1055    async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1056
1057    /// gets public key by it's hex value
1058    async fn get_public_key(
1059        &self,
1060        pubkey: &PublicKey,
1061    ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1062
1063    /// gets list of stored public keys in database
1064    async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1065
1066    /// Gets the latest generated P2PK signing key (most recently created)
1067    async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1068
1069    /// try to get secret key from p2pk signing key in localstore
1070    async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1071}
1072
1073/// Public key generated for proof signing
1074#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1075pub struct P2PKSigningKey {
1076    /// Public key
1077    pub pubkey: PublicKey,
1078    /// Derivation path
1079    pub derivation_path: DerivationPath,
1080    /// Derivation index
1081    pub derivation_index: u32,
1082    /// Created time
1083    pub created_time: u64,
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089    use crate::nuts::Id;
1090    use crate::secret::Secret;
1091
1092    #[test]
1093    fn test_transaction_id_from_hex() {
1094        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1095        let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1096        assert_eq!(transaction_id.to_string(), hex_str);
1097    }
1098
1099    #[test]
1100    fn test_transaction_id_from_hex_empty_string() {
1101        let hex_str = "";
1102        let res = TransactionId::from_hex(hex_str);
1103        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1104    }
1105
1106    #[test]
1107    fn test_transaction_id_from_hex_longer_string() {
1108        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1109        let res = TransactionId::from_hex(hex_str);
1110        assert!(matches!(res, Err(Error::InvalidTransactionId)));
1111    }
1112
1113    #[test]
1114    fn test_matches_conditions() {
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.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1129
1130        // Test matching mint_url
1131        assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1132        assert!(!proof_info.matches_conditions(
1133            &Some(MintUrl::from_str("https://different.com").unwrap()),
1134            &None,
1135            &None,
1136            &None
1137        ));
1138
1139        // Test matching unit
1140        assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1141        assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1142
1143        // Test matching state
1144        assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1145        assert!(proof_info.matches_conditions(
1146            &None,
1147            &None,
1148            &Some(vec![State::Unspent, State::Spent]),
1149            &None
1150        ));
1151        assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1152
1153        // Test with no conditions (should match)
1154        assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1155
1156        // Test with multiple conditions
1157        assert!(proof_info.matches_conditions(
1158            &Some(mint_url),
1159            &Some(CurrencyUnit::Sat),
1160            &Some(vec![State::Unspent]),
1161            &None
1162        ));
1163    }
1164
1165    #[test]
1166    fn test_matches_conditions_with_spending_conditions() {
1167        // This test would need to be expanded with actual SpendingConditions
1168        // implementation, but we can test the basic case where no spending
1169        // conditions are present
1170
1171        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1172        let proof = Proof::new(
1173            Amount::from(64),
1174            keyset_id,
1175            Secret::new("test_secret"),
1176            PublicKey::from_hex(
1177                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1178            )
1179            .unwrap(),
1180        );
1181
1182        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1183        let proof_info =
1184            ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1185
1186        // Test with empty spending conditions (should match when proof has none)
1187        assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1188
1189        // Test with non-empty spending conditions (should not match when proof has none)
1190        let dummy_condition = SpendingConditions::P2PKConditions {
1191            data: SecretKey::generate().public_key(),
1192            conditions: None,
1193        };
1194        assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1195    }
1196}