Skip to main content

cdk/wallet/
mod.rs

1#![doc = include_str!("./README.md")]
2
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::str::FromStr;
6use std::sync::Arc;
7use std::time::Duration;
8
9use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
10use bitcoin::Network;
11use cdk_common::amount::FeeAndAmounts;
12use cdk_common::database::{self, WalletDatabase};
13use cdk_common::parking_lot::RwLock;
14use cdk_common::subscription::WalletParams;
15use cdk_common::wallet::ProofInfo;
16use cdk_common::{PublicKey, SecretKey, SECP256K1};
17use getrandom::getrandom;
18pub use mint_connector::http_client::{
19    AuthHttpClient as BaseAuthHttpClient, HttpClient as BaseHttpClient,
20};
21use subscription::{ActiveSubscription, SubscriptionManager};
22use tokio::sync::RwLock as TokioRwLock;
23use tracing::instrument;
24use zeroize::Zeroize;
25
26use crate::amount::SplitTarget;
27use crate::dhke::construct_proofs;
28use crate::error::Error;
29use crate::fees::calculate_fee;
30use crate::mint_url::MintUrl;
31use crate::nuts::nut00::token::Token;
32use crate::nuts::nut17::Kind;
33use crate::nuts::{
34    nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proofs,
35    RestoreRequest, SpendingConditions, State,
36};
37use crate::wallet::mint_metadata_cache::MintMetadataCache;
38use crate::wallet::p2pk::{P2PK_ACCOUNT, P2PK_PURPOSE};
39use crate::Amount;
40
41mod auth;
42pub mod bip321;
43#[cfg(feature = "nostr")]
44mod nostr_backup;
45#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
46pub use mint_connector::TorHttpClient;
47mod balance;
48mod builder;
49mod issue;
50mod keysets;
51mod melt;
52mod mint_connector;
53mod mint_metadata_cache;
54#[cfg(feature = "npubcash")]
55mod npubcash;
56mod p2pk;
57pub mod payment_request;
58mod proofs;
59mod receive;
60mod reclaim;
61mod recovery;
62pub(crate) mod saga;
63mod send;
64#[cfg(not(target_arch = "wasm32"))]
65mod streams;
66pub mod subscription;
67mod swap;
68pub mod test_utils;
69mod transactions;
70pub mod util;
71pub mod wallet_repository;
72mod wallet_trait;
73
74pub use auth::{AuthMintConnector, AuthWallet};
75#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
76pub use bip321::resolve_bip353_payment_instruction;
77pub use bip321::{
78    parse_payment_instruction, Bip321UriBuilder, ParsedPaymentInstruction, PaymentRequestBip321Ext,
79};
80pub use builder::WalletBuilder;
81pub use cdk_common::wallet as types;
82pub use cdk_common::wallet::{ReceiveOptions, SendMemo, SendOptions};
83pub use keysets::KeysetFilter;
84pub use melt::{MeltConfirmOptions, MeltOutcome, PendingMelt, PreparedMelt};
85pub use mint_connector::transport::Transport as HttpTransport;
86pub use mint_connector::{
87    AuthHttpClient, HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector,
88};
89#[cfg(feature = "nostr")]
90pub use nostr_backup::{BackupOptions, BackupResult, RestoreOptions, RestoreResult};
91pub use payment_request::CreateRequestParams;
92#[cfg(feature = "nostr")]
93pub use payment_request::NostrWaitInfo;
94pub use recovery::RecoveryReport;
95pub use send::PreparedSend;
96#[cfg(all(feature = "npubcash", not(target_arch = "wasm32")))]
97pub use streams::npubcash::NpubCashProofStream;
98pub use types::{MeltQuote, MintQuote, SendKind};
99pub use wallet_repository::{TokenData, WalletConfig, WalletRepository, WalletRepositoryBuilder};
100
101use crate::nuts::nut00::ProofsMethods;
102
103/// CDK Wallet
104///
105/// The CDK [`Wallet`] is a high level cashu wallet.
106///
107/// A [`Wallet`] is for a single mint and single unit.
108///
109/// # Initialization
110///
111/// After creating a wallet, call [`Wallet::recover_incomplete_sagas`] to recover
112/// from interrupted operations (swap, send, receive, melt). This is required to
113/// prevent proofs from being stuck in reserved states after a crash.
114///
115/// For pending mint quotes, call [`Wallet::mint_unissued_quotes`] which checks
116/// quote states with the mint and mints available tokens. This makes network calls.
117#[derive(Debug, Clone)]
118pub struct Wallet {
119    /// Mint Url
120    pub mint_url: MintUrl,
121    /// Unit
122    pub unit: CurrencyUnit,
123    /// Storage backend
124    pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
125    /// Mint metadata cache for this mint (lock-free cached access to keys, keysets, and mint info)
126    pub metadata_cache: Arc<MintMetadataCache>,
127    /// The targeted amount of proofs to have at each size
128    pub target_proof_count: usize,
129    metadata_cache_ttl: Arc<RwLock<Option<Duration>>>,
130    auth_wallet: Arc<TokioRwLock<Option<AuthWallet>>>,
131    #[cfg(feature = "npubcash")]
132    npubcash_client: Arc<TokioRwLock<Option<Arc<cdk_npubcash::NpubCashClient>>>>,
133    seed: [u8; 64],
134    client: Arc<dyn MintConnector + Send + Sync>,
135    subscription: SubscriptionManager,
136}
137
138const ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
139
140/// Wallet Subscription filter
141#[derive(Debug, Clone)]
142pub enum WalletSubscription {
143    /// Proof subscription
144    ProofState(Vec<String>),
145    /// Mint quote subscription
146    Bolt11MintQuoteState(Vec<String>),
147    /// Melt quote subscription
148    Bolt11MeltQuoteState(Vec<String>),
149    /// Melt bolt12 quote subscription
150    Bolt12MeltQuoteState(Vec<String>),
151    /// Mint bolt12 quote subscription
152    Bolt12MintQuoteState(Vec<String>),
153    /// Custom melt quote subscription
154    MeltQuoteCustom(String, Vec<String>),
155}
156
157impl From<WalletSubscription> for WalletParams {
158    fn from(val: WalletSubscription) -> Self {
159        let mut buffer = vec![0u8; 10];
160
161        getrandom(&mut buffer).expect("Failed to generate random bytes");
162
163        let id = Arc::new(
164            buffer
165                .iter()
166                .map(|&byte| {
167                    let index = byte as usize % ALPHANUMERIC.len(); // 62 alphanumeric characters (A-Z, a-z, 0-9)
168                    ALPHANUMERIC[index] as char
169                })
170                .collect::<String>(),
171        );
172
173        match val {
174            WalletSubscription::ProofState(filters) => WalletParams {
175                filters,
176                kind: Kind::ProofState,
177                id,
178            },
179            WalletSubscription::Bolt11MintQuoteState(filters) => WalletParams {
180                filters,
181                kind: Kind::Bolt11MintQuote,
182                id,
183            },
184            WalletSubscription::Bolt11MeltQuoteState(filters) => WalletParams {
185                filters,
186                kind: Kind::Bolt11MeltQuote,
187                id,
188            },
189            WalletSubscription::Bolt12MintQuoteState(filters) => WalletParams {
190                filters,
191                kind: Kind::Bolt12MintQuote,
192                id,
193            },
194            WalletSubscription::Bolt12MeltQuoteState(filters) => WalletParams {
195                filters,
196                kind: Kind::Bolt12MeltQuote,
197                id,
198            },
199            WalletSubscription::MeltQuoteCustom(method, filters) => WalletParams {
200                filters,
201                kind: Kind::Custom(format!("{}_melt_quote", method)),
202                id,
203            },
204        }
205    }
206}
207
208pub use cdk_common::wallet::Restored;
209
210impl Wallet {
211    /// Create new [`Wallet`] using the builder pattern
212    /// # Synopsis
213    /// ```rust
214    /// use std::sync::Arc;
215    ///
216    /// use bitcoin::bip32::Xpriv;
217    /// use cdk::nuts::CurrencyUnit;
218    /// use cdk::wallet::{Wallet, WalletBuilder};
219    /// use cdk_sqlite::wallet::memory;
220    /// use rand::random;
221    ///
222    /// async fn test() -> anyhow::Result<()> {
223    ///     let seed = random::<[u8; 64]>();
224    ///     let mint_url = "https://fake.thesimplekid.dev";
225    ///     let unit = CurrencyUnit::Sat;
226    ///
227    ///     let localstore = memory::empty().await?;
228    ///     let wallet = WalletBuilder::new()
229    ///         .mint_url(mint_url.parse().unwrap())
230    ///         .unit(unit)
231    ///         .localstore(Arc::new(localstore))
232    ///         .seed(seed)
233    ///         .build();
234    ///     Ok(())
235    /// }
236    /// ```
237    pub fn new(
238        mint_url: &str,
239        unit: CurrencyUnit,
240        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
241        seed: [u8; 64],
242        target_proof_count: Option<usize>,
243    ) -> Result<Self, Error> {
244        let mint_url = MintUrl::from_str(mint_url)?;
245
246        WalletBuilder::new()
247            .mint_url(mint_url)
248            .unit(unit)
249            .localstore(localstore)
250            .seed(seed)
251            .target_proof_count(target_proof_count.unwrap_or(3))
252            .build()
253    }
254
255    /// Subscribe to events
256    pub async fn subscribe<T: Into<WalletParams>>(
257        &self,
258        query: T,
259    ) -> Result<ActiveSubscription, Error> {
260        self.subscription
261            .subscribe(self.mint_url.clone(), query.into())
262            .map_err(|e| Error::SubscriptionError(e.to_string()))
263    }
264
265    /// Subscribe to mint quote state changes for the given quote IDs and payment method
266    #[instrument(skip(self, method))]
267    pub async fn subscribe_mint_quote_state(
268        &self,
269        quote_ids: Vec<String>,
270        method: cdk_common::PaymentMethod,
271    ) -> Result<ActiveSubscription, Error> {
272        use cdk_common::nut00::KnownMethod;
273
274        let sub = match method {
275            cdk_common::PaymentMethod::Known(KnownMethod::Bolt11) => {
276                WalletSubscription::Bolt11MintQuoteState(quote_ids)
277            }
278            cdk_common::PaymentMethod::Known(KnownMethod::Bolt12) => {
279                WalletSubscription::Bolt12MintQuoteState(quote_ids)
280            }
281            cdk_common::PaymentMethod::Custom(_) => {
282                return Err(Error::InvalidPaymentMethod);
283            }
284        };
285        self.subscribe(sub).await
286    }
287
288    /// Fee required to redeem proof set
289    #[instrument(skip_all)]
290    pub async fn get_proofs_fee(
291        &self,
292        proofs: &Proofs,
293    ) -> Result<crate::fees::ProofsFeeBreakdown, Error> {
294        let proofs_per_keyset = proofs.count_by_keyset();
295        self.get_proofs_fee_by_count(proofs_per_keyset).await
296    }
297
298    /// Fee required to redeem proof set by count
299    pub async fn get_proofs_fee_by_count(
300        &self,
301        proofs_per_keyset: HashMap<Id, u64>,
302    ) -> Result<crate::fees::ProofsFeeBreakdown, Error> {
303        let mut fee_per_keyset = HashMap::new();
304        let metadata = self
305            .metadata_cache
306            .load(&self.localstore, &self.client, {
307                let ttl = self.metadata_cache_ttl.read();
308                *ttl
309            })
310            .await?;
311
312        for keyset_id in proofs_per_keyset.keys() {
313            let mint_keyset_info = metadata
314                .keysets
315                .get(keyset_id)
316                .ok_or(Error::UnknownKeySet)?;
317            fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
318        }
319
320        let fee_breakdown = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
321
322        Ok(fee_breakdown)
323    }
324
325    /// Get fee for count of proofs in a keyset
326    #[instrument(skip_all)]
327    pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
328        let input_fee_ppk = self
329            .metadata_cache
330            .load(&self.localstore, &self.client, {
331                let ttl = self.metadata_cache_ttl.read();
332                *ttl
333            })
334            .await?
335            .keysets
336            .get(keyset_id)
337            .ok_or(Error::UnknownKeySet)?
338            .input_fee_ppk;
339
340        let fee = (input_fee_ppk * count).div_ceil(1000);
341
342        Ok(Amount::from(fee))
343    }
344
345    /// Calculate fee for a given number of proofs with the specified keyset
346    #[instrument(skip(self))]
347    pub async fn calculate_fee(&self, proof_count: u64, keyset_id: Id) -> Result<Amount, Error> {
348        self.get_keyset_count_fee(&keyset_id, proof_count).await
349    }
350
351    /// Update Mint information and related entries in the event a mint changes
352    /// its URL
353    #[instrument(skip(self))]
354    pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
355        self.localstore
356            .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
357            .await?;
358
359        self.mint_url = new_mint_url;
360
361        Ok(())
362    }
363
364    /// Query mint for current mint information
365    #[instrument(skip(self))]
366    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
367        let mint_info = self
368            .metadata_cache
369            .load_from_mint(&self.localstore, &self.client)
370            .await?
371            .mint_info
372            .clone();
373
374        // If mint provides time make sure it is accurate
375        if let Some(mint_unix_time) = mint_info.time {
376            let current_unix_time = crate::util::unix_time();
377            if current_unix_time.abs_diff(mint_unix_time) > 30 {
378                tracing::warn!(
379                    "Mint time does match wallet time. Mint: {}, Wallet: {}",
380                    mint_unix_time,
381                    current_unix_time
382                );
383                return Err(Error::MintTimeExceedsTolerance);
384            }
385        }
386
387        // Create or update auth wallet
388        {
389            let mut auth_wallet = self.auth_wallet.write().await;
390            match &*auth_wallet {
391                Some(auth_wallet) => {
392                    let mut protected_endpoints = auth_wallet.protected_endpoints.write().await;
393                    *protected_endpoints = mint_info.protected_endpoints();
394
395                    if let Some(oidc_client) = mint_info
396                        .openid_discovery()
397                        .map(|url| crate::OidcClient::new(url, None))
398                    {
399                        auth_wallet.set_oidc_client(Some(oidc_client)).await;
400                    }
401                }
402                None => {
403                    tracing::info!("Mint has auth enabled creating auth wallet");
404
405                    let oidc_client = mint_info
406                        .openid_discovery()
407                        .map(|url| crate::OidcClient::new(url, None));
408                    let new_auth_wallet = AuthWallet::new(
409                        self.mint_url.clone(),
410                        None,
411                        self.localstore.clone(),
412                        self.metadata_cache.clone(),
413                        mint_info.protected_endpoints(),
414                        oidc_client,
415                    );
416                    *auth_wallet = Some(new_auth_wallet.clone());
417
418                    self.client
419                        .set_auth_wallet(Some(new_auth_wallet.clone()))
420                        .await;
421
422                    if let Err(e) = new_auth_wallet.refresh_keysets().await {
423                        tracing::error!("Could not fetch auth keysets: {}", e);
424                    }
425                }
426            }
427        }
428
429        tracing::trace!("Mint info updated for {}", self.mint_url);
430
431        Ok(Some(mint_info))
432    }
433
434    /// Load mint info from cache
435    ///
436    /// This is a helper function that loads the mint info from the metadata cache
437    /// using the configured TTL. Unlike `fetch_mint_info()`, this does not make
438    /// a network call if the cache is fresh.
439    #[instrument(skip(self))]
440    pub async fn load_mint_info(&self) -> Result<MintInfo, Error> {
441        let mint_info = self
442            .metadata_cache
443            .load(&self.localstore, &self.client, {
444                let ttl = self.metadata_cache_ttl.read();
445                *ttl
446            })
447            .await?
448            .mint_info
449            .clone();
450
451        Ok(mint_info)
452    }
453
454    /// Get amounts needed to refill proof state
455    #[instrument(skip(self))]
456    pub(crate) async fn amounts_needed_for_state_target(
457        &self,
458        fee_and_amounts: &FeeAndAmounts,
459    ) -> Result<Vec<Amount>, Error> {
460        let unspent_proofs = self
461            .get_proofs_with(Some(vec![State::Unspent]), None)
462            .await?;
463
464        let amounts_count: HashMap<u64, u64> =
465            unspent_proofs
466                .iter()
467                .fold(HashMap::new(), |mut acc, proof| {
468                    let amount = proof.amount;
469                    let counter = acc.entry(u64::from(amount)).or_insert(0);
470                    *counter += 1;
471                    acc
472                });
473
474        let needed_amounts =
475            fee_and_amounts
476                .amounts()
477                .iter()
478                .fold(Vec::new(), |mut acc, amount| {
479                    let count_needed = (self.target_proof_count as u64)
480                        .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
481
482                    for _i in 0..count_needed {
483                        acc.push(Amount::from(*amount));
484                    }
485
486                    acc
487                });
488        Ok(needed_amounts)
489    }
490
491    /// Determine [`SplitTarget`] for amount based on state
492    #[instrument(skip(self))]
493    async fn determine_split_target_values(
494        &self,
495        change_amount: Amount,
496        fee_and_amounts: &FeeAndAmounts,
497    ) -> Result<SplitTarget, Error> {
498        let mut amounts_needed_refill = self
499            .amounts_needed_for_state_target(fee_and_amounts)
500            .await?;
501
502        amounts_needed_refill.sort();
503
504        let mut values = Vec::new();
505
506        for amount in amounts_needed_refill {
507            let values_sum = Amount::try_sum(values.clone().into_iter())?;
508            if values_sum + amount <= change_amount {
509                values.push(amount);
510            }
511        }
512
513        Ok(SplitTarget::Values(values))
514    }
515
516    /// Restore
517    #[instrument(skip(self))]
518    pub async fn restore(&self) -> Result<Restored, Error> {
519        // Check that mint is in store of mints
520        if self
521            .localstore
522            .get_mint(self.mint_url.clone())
523            .await?
524            .is_none()
525        {
526            self.fetch_mint_info().await?;
527        }
528
529        let keysets = self.get_mint_keysets(KeysetFilter::All).await?;
530
531        let mut restored_result = Restored::default();
532
533        for keyset in keysets {
534            let keys = self.load_keyset_keys(keyset.id).await?;
535            let mut empty_batch = 0;
536            let mut start_counter = 0;
537            // Track the highest counter value that had a signature
538            let mut highest_counter: Option<u32> = None;
539
540            while empty_batch.lt(&3) {
541                let premint_secrets = PreMintSecrets::restore_batch(
542                    keyset.id,
543                    &self.seed,
544                    start_counter,
545                    start_counter + 100,
546                )?;
547
548                tracing::debug!(
549                    "Attempting to restore counter {}-{} for mint {} keyset {}",
550                    start_counter,
551                    start_counter + 100,
552                    self.mint_url,
553                    keyset.id
554                );
555
556                let restore_request = RestoreRequest {
557                    outputs: premint_secrets.blinded_messages(),
558                };
559
560                let response = self.client.post_restore(restore_request).await?;
561
562                if response.signatures.is_empty() {
563                    empty_batch += 1;
564                    start_counter += 100;
565                    continue;
566                }
567
568                // Build a map from blinded_secret to signature for O(1) lookup
569                // This ensures we match signatures to secrets correctly regardless of response order
570                let signature_map: HashMap<_, _> = response
571                    .outputs
572                    .iter()
573                    .zip(response.signatures.iter())
574                    .map(|(output, sig)| (output.blinded_secret, sig.clone()))
575                    .collect();
576
577                // Enumerate secrets to track their original index (which corresponds to counter value)
578                // and match signatures by blinded_secret to ensure correct pairing
579                let matched_secrets: Vec<_> = premint_secrets
580                    .secrets
581                    .iter()
582                    .enumerate()
583                    .filter_map(|(idx, p)| {
584                        signature_map
585                            .get(&p.blinded_message.blinded_secret)
586                            .map(|sig| (idx, p, sig.clone()))
587                    })
588                    .collect();
589
590                // Update highest counter based on matched indices
591                if let Some(&(max_idx, _, _)) = matched_secrets.last() {
592                    let counter_value = start_counter + max_idx as u32;
593                    highest_counter =
594                        Some(highest_counter.map_or(counter_value, |c| c.max(counter_value)));
595                }
596
597                // the response outputs and premint secrets should be the same after filtering
598                // blinded messages the mint did not have signatures for
599                if response.outputs.len() != matched_secrets.len() {
600                    return Err(Error::InvalidMintResponse(format!(
601                        "restore response outputs ({}) does not match premint secrets ({})",
602                        response.outputs.len(),
603                        matched_secrets.len()
604                    )));
605                }
606
607                // Extract signatures, rs, and secrets in matching order
608                // Each tuple (idx, premint, signature) ensures correct pairing
609                let proofs = construct_proofs(
610                    matched_secrets
611                        .iter()
612                        .map(|(_, _, sig)| sig.clone())
613                        .collect(),
614                    matched_secrets
615                        .iter()
616                        .map(|(_, p, _)| p.r.clone())
617                        .collect(),
618                    matched_secrets
619                        .iter()
620                        .map(|(_, p, _)| p.secret.clone())
621                        .collect(),
622                    &keys,
623                )?;
624
625                tracing::debug!("Restored {} proofs", proofs.len());
626
627                let states = self.check_proofs_spent(proofs.clone()).await?;
628
629                let (unspent_proofs, updated_restored) = proofs
630                    .into_iter()
631                    .zip(states)
632                    .filter_map(|(p, state)| {
633                        ProofInfo::new(p, self.mint_url.clone(), state.state, keyset.unit.clone())
634                            .ok()
635                    })
636                    .try_fold(
637                        (Vec::new(), restored_result),
638                        |(mut proofs, mut restored_result), proof_info| {
639                            match proof_info.state {
640                                State::Spent => {
641                                    restored_result.spent += proof_info.proof.amount;
642                                }
643                                State::Unspent =>  {
644                                    restored_result.unspent += proof_info.proof.amount;
645                                    proofs.push(proof_info);
646                                }
647                                State::Pending => {
648                                    restored_result.pending += proof_info.proof.amount;
649                                    proofs.push(proof_info);
650                                }
651                                _ => {
652                                    unreachable!("These states are unknown to the mint and cannot be returned")
653                                }
654                            }
655                            Ok::<(Vec<ProofInfo>, Restored), Error>((proofs, restored_result))
656                        },
657                    )?;
658
659                restored_result = updated_restored;
660
661                self.localstore
662                    .update_proofs(unspent_proofs, vec![])
663                    .await?;
664
665                empty_batch = 0;
666                start_counter += 100;
667            }
668
669            if let Some(highest) = highest_counter {
670                self.localstore
671                    .increment_keyset_counter(&keyset.id, highest + 1)
672                    .await?;
673                tracing::debug!(
674                    "Set keyset {} counter to {} after restore",
675                    keyset.id,
676                    highest + 1
677                );
678            }
679        }
680        Ok(restored_result)
681    }
682
683    /// Verify all proofs in token have meet the required spend
684    /// Can be used to allow a wallet to accept payments offline while reducing
685    /// the risk of claiming back to the limits let by the spending_conditions
686    #[instrument(skip(self, token))]
687    pub async fn verify_token_p2pk(
688        &self,
689        token: &Token,
690        spending_conditions: SpendingConditions,
691    ) -> Result<(), Error> {
692        let (refund_keys, pubkeys, locktime, num_sigs) = match spending_conditions {
693            SpendingConditions::P2PKConditions { data, conditions } => {
694                let mut pubkeys = vec![data];
695
696                match conditions {
697                    Some(conditions) => {
698                        pubkeys.extend(conditions.pubkeys.unwrap_or_default());
699
700                        (
701                            conditions.refund_keys,
702                            Some(pubkeys),
703                            conditions.locktime,
704                            conditions.num_sigs,
705                        )
706                    }
707                    None => (None, Some(pubkeys), None, None),
708                }
709            }
710            SpendingConditions::HTLCConditions {
711                conditions,
712                data: _,
713            } => match conditions {
714                Some(conditions) => (
715                    conditions.refund_keys,
716                    conditions.pubkeys,
717                    conditions.locktime,
718                    conditions.num_sigs,
719                ),
720                None => (None, None, None, None),
721            },
722        };
723
724        if refund_keys.is_some() && locktime.is_none() {
725            tracing::warn!(
726                "Invalid spending conditions set: Locktime must be set if refund keys are allowed"
727            );
728            return Err(Error::InvalidSpendConditions(
729                "Must set locktime".to_string(),
730            ));
731        }
732        if token.mint_url()? != self.mint_url {
733            return Err(Error::IncorrectWallet(format!(
734                "Should be {} not {}",
735                self.mint_url,
736                token.mint_url()?
737            )));
738        }
739        // We need the keysets information to properly convert from token proof to proof
740        let keysets_info = self.load_mint_keysets().await?;
741        let proofs = token.proofs(&keysets_info)?;
742
743        for proof in proofs {
744            let secret: nut10::Secret = (&proof.secret).try_into()?;
745
746            let proof_conditions: SpendingConditions = secret.try_into()?;
747
748            if num_sigs.ne(&proof_conditions.num_sigs()) {
749                tracing::debug!(
750                    "Spending condition requires: {:?} sigs proof secret specifies: {:?}",
751                    num_sigs,
752                    proof_conditions.num_sigs()
753                );
754
755                return Err(Error::P2PKConditionsNotMet(
756                    "Num sigs did not match spending condition".to_string(),
757                ));
758            }
759
760            let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default();
761            let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default();
762
763            // Check the Proof has the required pubkeys
764            if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
765                || !proof_pubkeys
766                    .iter()
767                    .all(|pubkey| spending_condition_pubkeys.contains(pubkey))
768            {
769                tracing::debug!("Proof did not included Publickeys meeting condition");
770                tracing::debug!("{:?}", proof_pubkeys);
771                tracing::debug!("{:?}", spending_condition_pubkeys);
772                return Err(Error::P2PKConditionsNotMet(
773                    "Pubkeys in proof not allowed by spending condition".to_string(),
774                ));
775            }
776
777            // If spending condition refund keys is allowed (Some(Empty Vec))
778            // If spending conition refund keys is allowed to restricted set of keys check
779            // it is one of them Check that proof locktime is > condition
780            // locktime
781
782            if let Some(proof_refund_keys) = proof_conditions.refund_keys() {
783                let proof_locktime = proof_conditions
784                    .locktime()
785                    .ok_or(Error::LocktimeNotProvided)?;
786
787                if let (Some(condition_refund_keys), Some(condition_locktime)) =
788                    (&refund_keys, locktime)
789                {
790                    // Proof locktime must be greater then condition locktime to ensure it
791                    // cannot be claimed back
792                    if proof_locktime.lt(&condition_locktime) {
793                        return Err(Error::P2PKConditionsNotMet(
794                            "Proof locktime less then required".to_string(),
795                        ));
796                    }
797
798                    // A non empty condition refund key list is used as a restricted set of keys
799                    // returns are allowed to An empty list means the
800                    // proof can be refunded to anykey set in the secret
801                    if !condition_refund_keys.is_empty()
802                        && !proof_refund_keys
803                            .iter()
804                            .all(|refund_key| condition_refund_keys.contains(refund_key))
805                    {
806                        return Err(Error::P2PKConditionsNotMet(
807                            "Refund Key not allowed".to_string(),
808                        ));
809                    }
810                } else {
811                    // Spending conditions does not allow refund keys
812                    return Err(Error::P2PKConditionsNotMet(
813                        "Spending condition does not allow refund keys".to_string(),
814                    ));
815                }
816            }
817        }
818
819        Ok(())
820    }
821
822    /// Verify all proofs in token have a valid DLEQ proof
823    #[instrument(skip(self, token))]
824    pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
825        let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
826
827        // TODO: Get mint url
828        // if mint_url != &self.mint_url {
829        //     return Err(Error::IncorrectWallet(format!(
830        //         "Should be {} not {}",
831        //         self.mint_url, mint_url
832        //     )));
833        // }
834
835        // We need the keysets information to properly convert from token proof to proof
836        let keysets_info = self.load_mint_keysets().await?;
837        let proofs = token.proofs(&keysets_info)?;
838        for proof in proofs {
839            let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
840                Some(keys) => keys.amount_key(proof.amount),
841                None => {
842                    let keys = self.load_keyset_keys(proof.keyset_id).await?;
843
844                    let key = keys.amount_key(proof.amount);
845                    keys_cache.insert(proof.keyset_id, keys);
846
847                    key
848                }
849            }
850            .ok_or(Error::AmountKey)?;
851
852            proof
853                .verify_dleq(mint_pubkey)
854                .map_err(|_| Error::CouldNotVerifyDleq)?;
855        }
856
857        Ok(())
858    }
859
860    /// Set the client (MintConnector) for this wallet
861    ///
862    /// This allows updating the connector without recreating the wallet.
863    pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
864        self.client = client;
865    }
866
867    /// Get the connector used by this wallet.
868    pub fn mint_connector(&self) -> Arc<dyn MintConnector + Send + Sync> {
869        self.client.clone()
870    }
871
872    /// Set the target proof count for this wallet
873    ///
874    /// This controls how many proofs of each denomination the wallet tries to maintain.
875    pub fn set_target_proof_count(&mut self, count: usize) {
876        self.target_proof_count = count;
877    }
878
879    /// generates and stores public key in database
880    pub async fn generate_public_key(&self) -> Result<PublicKey, Error> {
881        let public_keys = self.localstore.list_p2pk_keys().await?;
882
883        let mut last_derivation_index = 0;
884
885        for public_key in public_keys {
886            if public_key.derivation_index >= last_derivation_index {
887                last_derivation_index = public_key.derivation_index + 1;
888            }
889        }
890
891        let derivation_path = DerivationPath::from(vec![
892            ChildNumber::from_hardened_idx(P2PK_PURPOSE)?,
893            ChildNumber::from_hardened_idx(P2PK_ACCOUNT)?,
894            ChildNumber::from_hardened_idx(0)?,
895            ChildNumber::from_hardened_idx(0)?,
896            ChildNumber::from_normal_idx(last_derivation_index)?,
897        ]);
898
899        let pubkey = p2pk::generate_public_key(&derivation_path, &self.seed).await?;
900
901        self.localstore
902            .add_p2pk_key(&pubkey, derivation_path, last_derivation_index)
903            .await?;
904
905        Ok(pubkey)
906    }
907
908    /// gets public key by it's hex value
909    pub async fn get_public_key(
910        &self,
911        pubkey: &PublicKey,
912    ) -> Result<Option<cdk_common::wallet::P2PKSigningKey>, database::Error> {
913        self.localstore.get_p2pk_key(pubkey).await
914    }
915
916    /// gets list of stored public keys in database
917    pub async fn get_public_keys(
918        &self,
919    ) -> Result<Vec<cdk_common::wallet::P2PKSigningKey>, database::Error> {
920        self.localstore.list_p2pk_keys().await
921    }
922
923    /// Gets the latest generated P2PK signing key (most recently created)
924    pub async fn get_latest_public_key(
925        &self,
926    ) -> Result<Option<cdk_common::wallet::P2PKSigningKey>, database::Error> {
927        self.localstore.latest_p2pk().await
928    }
929
930    /// try to get secret key from p2pk signing key in localstore
931    async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Error> {
932        let signing = self.localstore.get_p2pk_key(pubkey).await?;
933        if let Some(signing) = signing {
934            let xpriv = Xpriv::new_master(Network::Bitcoin, &self.seed)?;
935            return Ok(Some(SecretKey::from(
936                xpriv
937                    .derive_priv(&SECP256K1, &signing.derivation_path)?
938                    .private_key,
939            )));
940        }
941
942        Ok(None)
943    }
944}
945
946impl Drop for Wallet {
947    fn drop(&mut self) {
948        self.seed.zeroize();
949    }
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955    use crate::nuts::{BlindSignature, BlindedMessage, PreMint, PreMintSecrets};
956    use crate::secret::Secret;
957
958    /// Test that restore signature matching works correctly when response is in order
959    #[test]
960    fn test_restore_signature_matching_in_order() {
961        // Create test data with 3 premint secrets
962        let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
963
964        // Generate deterministic keys for testing
965        let secret1 = Secret::generate();
966        let secret2 = Secret::generate();
967        let secret3 = Secret::generate();
968
969        let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
970        let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
971        let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
972
973        let premint1 = PreMint {
974            blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
975            secret: secret1.clone(),
976            r: r1.clone(),
977            amount: Amount::from(1),
978        };
979        let premint2 = PreMint {
980            blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
981            secret: secret2.clone(),
982            r: r2.clone(),
983            amount: Amount::from(2),
984        };
985        let premint3 = PreMint {
986            blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
987            secret: secret3.clone(),
988            r: r3.clone(),
989            amount: Amount::from(4),
990        };
991
992        let premint_secrets = PreMintSecrets {
993            secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
994            keyset_id,
995        };
996
997        // Create mock signatures (just need the structure, not real signatures)
998        let sig1 = BlindSignature {
999            amount: Amount::from(1),
1000            keyset_id,
1001            c: blinded1, // Using blinded as placeholder for signature
1002            dleq: None,
1003        };
1004        let sig2 = BlindSignature {
1005            amount: Amount::from(2),
1006            keyset_id,
1007            c: blinded2,
1008            dleq: None,
1009        };
1010        let sig3 = BlindSignature {
1011            amount: Amount::from(4),
1012            keyset_id,
1013            c: blinded3,
1014            dleq: None,
1015        };
1016
1017        // Response in same order as request
1018        let response_outputs = [
1019            premint1.blinded_message.clone(),
1020            premint2.blinded_message.clone(),
1021            premint3.blinded_message.clone(),
1022        ];
1023        let response_signatures = [sig1.clone(), sig2.clone(), sig3.clone()];
1024
1025        // Apply the matching logic (same as in restore)
1026        let signature_map: HashMap<_, _> = response_outputs
1027            .iter()
1028            .zip(response_signatures.iter())
1029            .map(|(output, sig)| (output.blinded_secret, sig.clone()))
1030            .collect();
1031
1032        let matched_secrets: Vec<_> = premint_secrets
1033            .secrets
1034            .iter()
1035            .enumerate()
1036            .filter_map(|(idx, p)| {
1037                signature_map
1038                    .get(&p.blinded_message.blinded_secret)
1039                    .map(|sig| (idx, p, sig.clone()))
1040            })
1041            .collect();
1042
1043        // Verify all 3 matched
1044        assert_eq!(matched_secrets.len(), 3);
1045
1046        // Verify correct pairing by checking amounts match
1047        assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
1048        assert_eq!(matched_secrets[1].2.amount, Amount::from(2));
1049        assert_eq!(matched_secrets[2].2.amount, Amount::from(4));
1050
1051        // Verify indices are preserved
1052        assert_eq!(matched_secrets[0].0, 0);
1053        assert_eq!(matched_secrets[1].0, 1);
1054        assert_eq!(matched_secrets[2].0, 2);
1055    }
1056
1057    /// Test that restore signature matching works correctly when response is OUT of order
1058    /// This is the critical test that verifies the fix for TokenNotVerified
1059    #[test]
1060    fn test_restore_signature_matching_out_of_order() {
1061        let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
1062
1063        let secret1 = Secret::generate();
1064        let secret2 = Secret::generate();
1065        let secret3 = Secret::generate();
1066
1067        let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
1068        let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
1069        let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
1070
1071        let premint1 = PreMint {
1072            blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
1073            secret: secret1.clone(),
1074            r: r1.clone(),
1075            amount: Amount::from(1),
1076        };
1077        let premint2 = PreMint {
1078            blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
1079            secret: secret2.clone(),
1080            r: r2.clone(),
1081            amount: Amount::from(2),
1082        };
1083        let premint3 = PreMint {
1084            blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
1085            secret: secret3.clone(),
1086            r: r3.clone(),
1087            amount: Amount::from(4),
1088        };
1089
1090        let premint_secrets = PreMintSecrets {
1091            secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
1092            keyset_id,
1093        };
1094
1095        let sig1 = BlindSignature {
1096            amount: Amount::from(1),
1097            keyset_id,
1098            c: blinded1,
1099            dleq: None,
1100        };
1101        let sig2 = BlindSignature {
1102            amount: Amount::from(2),
1103            keyset_id,
1104            c: blinded2,
1105            dleq: None,
1106        };
1107        let sig3 = BlindSignature {
1108            amount: Amount::from(4),
1109            keyset_id,
1110            c: blinded3,
1111            dleq: None,
1112        };
1113
1114        // Response in REVERSED order (simulating out-of-order response from mint)
1115        let response_outputs = [
1116            premint3.blinded_message.clone(), // index 2 first
1117            premint1.blinded_message.clone(), // index 0 second
1118            premint2.blinded_message.clone(), // index 1 third
1119        ];
1120        let response_signatures = [sig3.clone(), sig1.clone(), sig2.clone()];
1121
1122        // Apply the matching logic (same as in restore)
1123        let signature_map: HashMap<_, _> = response_outputs
1124            .iter()
1125            .zip(response_signatures.iter())
1126            .map(|(output, sig)| (output.blinded_secret, sig.clone()))
1127            .collect();
1128
1129        let matched_secrets: Vec<_> = premint_secrets
1130            .secrets
1131            .iter()
1132            .enumerate()
1133            .filter_map(|(idx, p)| {
1134                signature_map
1135                    .get(&p.blinded_message.blinded_secret)
1136                    .map(|sig| (idx, p, sig.clone()))
1137            })
1138            .collect();
1139
1140        // Verify all 3 matched
1141        assert_eq!(matched_secrets.len(), 3);
1142
1143        // Critical: Even though response was out of order, signatures should be
1144        // correctly paired with their corresponding premint secrets
1145        // matched_secrets should be in premint order (0, 1, 2) with correct signatures
1146        assert_eq!(matched_secrets[0].0, 0); // First premint (amount 1)
1147        assert_eq!(matched_secrets[0].2.amount, Amount::from(1)); // Correct signature
1148
1149        assert_eq!(matched_secrets[1].0, 1); // Second premint (amount 2)
1150        assert_eq!(matched_secrets[1].2.amount, Amount::from(2)); // Correct signature
1151
1152        assert_eq!(matched_secrets[2].0, 2); // Third premint (amount 4)
1153        assert_eq!(matched_secrets[2].2.amount, Amount::from(4)); // Correct signature
1154    }
1155
1156    /// Test that restore handles partial responses correctly
1157    #[test]
1158    fn test_restore_signature_matching_partial_response() {
1159        let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
1160
1161        let secret1 = Secret::generate();
1162        let secret2 = Secret::generate();
1163        let secret3 = Secret::generate();
1164
1165        let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
1166        let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
1167        let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
1168
1169        let premint1 = PreMint {
1170            blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
1171            secret: secret1.clone(),
1172            r: r1.clone(),
1173            amount: Amount::from(1),
1174        };
1175        let premint2 = PreMint {
1176            blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
1177            secret: secret2.clone(),
1178            r: r2.clone(),
1179            amount: Amount::from(2),
1180        };
1181        let premint3 = PreMint {
1182            blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
1183            secret: secret3.clone(),
1184            r: r3.clone(),
1185            amount: Amount::from(4),
1186        };
1187
1188        let premint_secrets = PreMintSecrets {
1189            secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
1190            keyset_id,
1191        };
1192
1193        let sig1 = BlindSignature {
1194            amount: Amount::from(1),
1195            keyset_id,
1196            c: blinded1,
1197            dleq: None,
1198        };
1199        let sig3 = BlindSignature {
1200            amount: Amount::from(4),
1201            keyset_id,
1202            c: blinded3,
1203            dleq: None,
1204        };
1205
1206        // Response only has signatures for premint1 and premint3 (gap at premint2)
1207        // Also out of order
1208        let response_outputs = [
1209            premint3.blinded_message.clone(),
1210            premint1.blinded_message.clone(),
1211        ];
1212        let response_signatures = [sig3.clone(), sig1.clone()];
1213
1214        let signature_map: HashMap<_, _> = response_outputs
1215            .iter()
1216            .zip(response_signatures.iter())
1217            .map(|(output, sig)| (output.blinded_secret, sig.clone()))
1218            .collect();
1219
1220        let matched_secrets: Vec<_> = premint_secrets
1221            .secrets
1222            .iter()
1223            .enumerate()
1224            .filter_map(|(idx, p)| {
1225                signature_map
1226                    .get(&p.blinded_message.blinded_secret)
1227                    .map(|sig| (idx, p, sig.clone()))
1228            })
1229            .collect();
1230
1231        // Only 2 should match
1232        assert_eq!(matched_secrets.len(), 2);
1233
1234        // Verify correct pairing despite gap and out-of-order response
1235        assert_eq!(matched_secrets[0].0, 0); // First premint (amount 1)
1236        assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
1237
1238        assert_eq!(matched_secrets[1].0, 2); // Third premint (amount 4), index 1 skipped
1239        assert_eq!(matched_secrets[1].2.amount, Amount::from(4));
1240    }
1241}