cdk/wallet/
mod.rs

1#![doc = include_str!("./README.md")]
2
3use std::collections::HashMap;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use cdk_common::database::{self, WalletDatabase};
8use cdk_common::subscription::Params;
9use getrandom::getrandom;
10use subscription::{ActiveSubscription, SubscriptionManager};
11#[cfg(feature = "auth")]
12use tokio::sync::RwLock;
13use tracing::instrument;
14use zeroize::Zeroize;
15
16use crate::amount::SplitTarget;
17use crate::dhke::construct_proofs;
18use crate::error::Error;
19use crate::fees::calculate_fee;
20use crate::mint_url::MintUrl;
21use crate::nuts::nut00::token::Token;
22use crate::nuts::nut17::Kind;
23use crate::nuts::{
24    nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs,
25    RestoreRequest, SpendingConditions, State,
26};
27use crate::types::ProofInfo;
28use crate::util::unix_time;
29use crate::Amount;
30#[cfg(feature = "auth")]
31use crate::OidcClient;
32
33#[cfg(feature = "auth")]
34mod auth;
35mod balance;
36mod builder;
37mod issue;
38mod keysets;
39mod melt;
40mod mint_connector;
41pub mod multi_mint_wallet;
42pub mod payment_request;
43mod proofs;
44mod receive;
45mod send;
46#[cfg(not(target_arch = "wasm32"))]
47mod streams;
48pub mod subscription;
49mod swap;
50mod transactions;
51pub mod util;
52
53#[cfg(feature = "auth")]
54pub use auth::{AuthMintConnector, AuthWallet};
55pub use builder::WalletBuilder;
56pub use cdk_common::wallet as types;
57#[cfg(feature = "auth")]
58pub use mint_connector::http_client::AuthHttpClient as BaseAuthHttpClient;
59pub use mint_connector::http_client::HttpClient as BaseHttpClient;
60pub use mint_connector::transport::Transport as HttpTransport;
61#[cfg(feature = "auth")]
62pub use mint_connector::AuthHttpClient;
63pub use mint_connector::{HttpClient, MintConnector};
64pub use multi_mint_wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet};
65pub use receive::ReceiveOptions;
66pub use send::{PreparedSend, SendMemo, SendOptions};
67pub use types::{MeltQuote, MintQuote, SendKind};
68
69use crate::nuts::nut00::ProofsMethods;
70
71/// CDK Wallet
72///
73/// The CDK [`Wallet`] is a high level cashu wallet.
74///
75/// A [`Wallet`] is for a single mint and single unit.
76#[derive(Debug, Clone)]
77pub struct Wallet {
78    /// Mint Url
79    pub mint_url: MintUrl,
80    /// Unit
81    pub unit: CurrencyUnit,
82    /// Storage backend
83    pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
84    /// The targeted amount of proofs to have at each size
85    pub target_proof_count: usize,
86    #[cfg(feature = "auth")]
87    auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
88    seed: [u8; 64],
89    client: Arc<dyn MintConnector + Send + Sync>,
90    subscription: SubscriptionManager,
91}
92
93const ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
94
95/// Wallet Subscription filter
96#[derive(Debug, Clone)]
97pub enum WalletSubscription {
98    /// Proof subscription
99    ProofState(Vec<String>),
100    /// Mint quote subscription
101    Bolt11MintQuoteState(Vec<String>),
102    /// Melt quote subscription
103    Bolt11MeltQuoteState(Vec<String>),
104    /// Mint bolt12 quote subscription
105    Bolt12MintQuoteState(Vec<String>),
106}
107
108impl From<WalletSubscription> for Params {
109    fn from(val: WalletSubscription) -> Self {
110        let mut buffer = vec![0u8; 10];
111
112        getrandom(&mut buffer).expect("Failed to generate random bytes");
113
114        let id = buffer
115            .iter()
116            .map(|&byte| {
117                let index = byte as usize % ALPHANUMERIC.len(); // 62 alphanumeric characters (A-Z, a-z, 0-9)
118                ALPHANUMERIC[index] as char
119            })
120            .collect::<String>();
121
122        match val {
123            WalletSubscription::ProofState(filters) => Params {
124                filters,
125                kind: Kind::ProofState,
126                id: id.into(),
127            },
128            WalletSubscription::Bolt11MintQuoteState(filters) => Params {
129                filters,
130                kind: Kind::Bolt11MintQuote,
131                id: id.into(),
132            },
133            WalletSubscription::Bolt11MeltQuoteState(filters) => Params {
134                filters,
135                kind: Kind::Bolt11MeltQuote,
136                id: id.into(),
137            },
138            WalletSubscription::Bolt12MintQuoteState(filters) => Params {
139                filters,
140                kind: Kind::Bolt12MintQuote,
141                id: id.into(),
142            },
143        }
144    }
145}
146
147impl Wallet {
148    /// Create new [`Wallet`] using the builder pattern
149    /// # Synopsis
150    /// ```rust
151    /// use bitcoin::bip32::Xpriv;
152    /// use std::sync::Arc;
153    ///
154    /// use cdk::nuts::CurrencyUnit;
155    /// use cdk::wallet::{Wallet, WalletBuilder};
156    /// use cdk_sqlite::wallet::memory;
157    /// use rand::random;
158    ///
159    /// async fn test() -> anyhow::Result<()> {
160    ///     let seed = random::<[u8; 64]>();
161    ///     let mint_url = "https://fake.thesimplekid.dev";
162    ///     let unit = CurrencyUnit::Sat;
163    ///
164    ///     let localstore = memory::empty().await?;
165    ///     let wallet = WalletBuilder::new()
166    ///         .mint_url(mint_url.parse().unwrap())
167    ///         .unit(unit)
168    ///         .localstore(Arc::new(localstore))
169    ///         .seed(seed)
170    ///         .build();
171    ///     Ok(())
172    /// }
173    /// ```
174    pub fn new(
175        mint_url: &str,
176        unit: CurrencyUnit,
177        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
178        seed: [u8; 64],
179        target_proof_count: Option<usize>,
180    ) -> Result<Self, Error> {
181        let mint_url = MintUrl::from_str(mint_url)?;
182
183        WalletBuilder::new()
184            .mint_url(mint_url)
185            .unit(unit)
186            .localstore(localstore)
187            .seed(seed)
188            .target_proof_count(target_proof_count.unwrap_or(3))
189            .build()
190    }
191
192    /// Subscribe to events
193    pub async fn subscribe<T: Into<Params>>(&self, query: T) -> ActiveSubscription {
194        self.subscription
195            .subscribe(self.mint_url.clone(), query.into(), Arc::new(self.clone()))
196            .await
197    }
198
199    /// Fee required for proof set
200    #[instrument(skip_all)]
201    pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
202        let proofs_per_keyset = proofs.count_by_keyset();
203        self.get_proofs_fee_by_count(proofs_per_keyset).await
204    }
205
206    /// Fee required for proof set by count
207    pub async fn get_proofs_fee_by_count(
208        &self,
209        proofs_per_keyset: HashMap<Id, u64>,
210    ) -> Result<Amount, Error> {
211        let mut fee_per_keyset = HashMap::new();
212
213        for keyset_id in proofs_per_keyset.keys() {
214            let mint_keyset_info = self
215                .localstore
216                .get_keyset_by_id(keyset_id)
217                .await?
218                .ok_or(Error::UnknownKeySet)?;
219            fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
220        }
221
222        let fee = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
223
224        Ok(fee)
225    }
226
227    /// Get fee for count of proofs in a keyset
228    #[instrument(skip_all)]
229    pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
230        let input_fee_ppk = self
231            .localstore
232            .get_keyset_by_id(keyset_id)
233            .await?
234            .ok_or(Error::UnknownKeySet)?
235            .input_fee_ppk;
236
237        let fee = (input_fee_ppk * count).div_ceil(1000);
238
239        Ok(Amount::from(fee))
240    }
241
242    /// Update Mint information and related entries in the event a mint changes
243    /// its URL
244    #[instrument(skip(self))]
245    pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
246        // Update the mint URL in the wallet DB
247        self.localstore
248            .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
249            .await?;
250
251        // Update the mint URL in the wallet struct field
252        self.mint_url = new_mint_url;
253
254        Ok(())
255    }
256
257    /// Query mint for current mint information
258    #[instrument(skip(self))]
259    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
260        match self.client.get_mint_info().await {
261            Ok(mint_info) => {
262                // If mint provides time make sure it is accurate
263                if let Some(mint_unix_time) = mint_info.time {
264                    let current_unix_time = unix_time();
265                    if current_unix_time.abs_diff(mint_unix_time) > 30 {
266                        tracing::warn!(
267                            "Mint time does match wallet time. Mint: {}, Wallet: {}",
268                            mint_unix_time,
269                            current_unix_time
270                        );
271                        return Err(Error::MintTimeExceedsTolerance);
272                    }
273                }
274
275                // Create or update auth wallet
276                #[cfg(feature = "auth")]
277                {
278                    let mut auth_wallet = self.auth_wallet.write().await;
279                    match &*auth_wallet {
280                        Some(auth_wallet) => {
281                            let mut protected_endpoints =
282                                auth_wallet.protected_endpoints.write().await;
283                            *protected_endpoints = mint_info.protected_endpoints();
284
285                            if let Some(oidc_client) = mint_info
286                                .openid_discovery()
287                                .map(|url| OidcClient::new(url, None))
288                            {
289                                auth_wallet.set_oidc_client(Some(oidc_client)).await;
290                            }
291                        }
292                        None => {
293                            tracing::info!("Mint has auth enabled creating auth wallet");
294
295                            let oidc_client = mint_info
296                                .openid_discovery()
297                                .map(|url| OidcClient::new(url, None));
298                            let new_auth_wallet = AuthWallet::new(
299                                self.mint_url.clone(),
300                                None,
301                                self.localstore.clone(),
302                                mint_info.protected_endpoints(),
303                                oidc_client,
304                            );
305                            *auth_wallet = Some(new_auth_wallet.clone());
306
307                            self.client.set_auth_wallet(Some(new_auth_wallet)).await;
308                        }
309                    }
310                }
311
312                self.localstore
313                    .add_mint(self.mint_url.clone(), Some(mint_info.clone()))
314                    .await?;
315
316                tracing::trace!("Mint info updated for {}", self.mint_url);
317
318                Ok(Some(mint_info))
319            }
320            Err(err) => {
321                tracing::warn!("Could not get mint info {}", err);
322                Ok(None)
323            }
324        }
325    }
326
327    /// Get amounts needed to refill proof state
328    #[instrument(skip(self))]
329    pub async fn amounts_needed_for_state_target(&self) -> Result<Vec<Amount>, Error> {
330        let unspent_proofs = self.get_unspent_proofs().await?;
331
332        let amounts_count: HashMap<usize, usize> =
333            unspent_proofs
334                .iter()
335                .fold(HashMap::new(), |mut acc, proof| {
336                    let amount = proof.amount;
337                    let counter = acc.entry(u64::from(amount) as usize).or_insert(0);
338                    *counter += 1;
339                    acc
340                });
341
342        let all_possible_amounts: Vec<usize> = (0..32).map(|i| 2usize.pow(i as u32)).collect();
343
344        let needed_amounts = all_possible_amounts
345            .iter()
346            .fold(Vec::new(), |mut acc, amount| {
347                let count_needed: usize = self
348                    .target_proof_count
349                    .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
350
351                for _i in 0..count_needed {
352                    acc.push(Amount::from(*amount as u64));
353                }
354
355                acc
356            });
357        Ok(needed_amounts)
358    }
359
360    /// Determine [`SplitTarget`] for amount based on state
361    #[instrument(skip(self))]
362    async fn determine_split_target_values(
363        &self,
364        change_amount: Amount,
365    ) -> Result<SplitTarget, Error> {
366        let mut amounts_needed_refill = self.amounts_needed_for_state_target().await?;
367
368        amounts_needed_refill.sort();
369
370        let mut values = Vec::new();
371
372        for amount in amounts_needed_refill {
373            let values_sum = Amount::try_sum(values.clone().into_iter())?;
374            if values_sum + amount <= change_amount {
375                values.push(amount);
376            }
377        }
378
379        Ok(SplitTarget::Values(values))
380    }
381
382    /// Restore
383    #[instrument(skip(self))]
384    pub async fn restore(&self) -> Result<Amount, Error> {
385        // Check that mint is in store of mints
386        if self
387            .localstore
388            .get_mint(self.mint_url.clone())
389            .await?
390            .is_none()
391        {
392            self.fetch_mint_info().await?;
393        }
394
395        let keysets = self.load_mint_keysets().await?;
396
397        let mut restored_value = Amount::ZERO;
398
399        for keyset in keysets {
400            let keys = self.load_keyset_keys(keyset.id).await?;
401            let mut empty_batch = 0;
402            let mut start_counter = 0;
403
404            while empty_batch.lt(&3) {
405                let premint_secrets = PreMintSecrets::restore_batch(
406                    keyset.id,
407                    &self.seed,
408                    start_counter,
409                    start_counter + 100,
410                )?;
411
412                tracing::debug!(
413                    "Attempting to restore counter {}-{} for mint {} keyset {}",
414                    start_counter,
415                    start_counter + 100,
416                    self.mint_url,
417                    keyset.id
418                );
419
420                let restore_request = RestoreRequest {
421                    outputs: premint_secrets.blinded_messages(),
422                };
423
424                let response = self.client.post_restore(restore_request).await?;
425
426                if response.signatures.is_empty() {
427                    empty_batch += 1;
428                    start_counter += 100;
429                    continue;
430                }
431
432                let premint_secrets: Vec<_> = premint_secrets
433                    .secrets
434                    .iter()
435                    .filter(|p| response.outputs.contains(&p.blinded_message))
436                    .collect();
437
438                // the response outputs and premint secrets should be the same after filtering
439                // blinded messages the mint did not have signatures for
440                assert_eq!(response.outputs.len(), premint_secrets.len());
441
442                let proofs = construct_proofs(
443                    response.signatures,
444                    premint_secrets.iter().map(|p| p.r.clone()).collect(),
445                    premint_secrets.iter().map(|p| p.secret.clone()).collect(),
446                    &keys,
447                )?;
448
449                tracing::debug!("Restored {} proofs", proofs.len());
450
451                self.localstore
452                    .increment_keyset_counter(&keyset.id, proofs.len() as u32)
453                    .await?;
454
455                let states = self.check_proofs_spent(proofs.clone()).await?;
456
457                let unspent_proofs: Vec<Proof> = proofs
458                    .iter()
459                    .zip(states)
460                    .filter(|(_, state)| !state.state.eq(&State::Spent))
461                    .map(|(p, _)| p)
462                    .cloned()
463                    .collect();
464
465                restored_value += unspent_proofs.total_amount()?;
466
467                let unspent_proofs = unspent_proofs
468                    .into_iter()
469                    .map(|proof| {
470                        ProofInfo::new(
471                            proof,
472                            self.mint_url.clone(),
473                            State::Unspent,
474                            keyset.unit.clone(),
475                        )
476                    })
477                    .collect::<Result<Vec<ProofInfo>, _>>()?;
478
479                self.localstore
480                    .update_proofs(unspent_proofs, vec![])
481                    .await?;
482
483                empty_batch = 0;
484                start_counter += 100;
485            }
486        }
487        Ok(restored_value)
488    }
489
490    /// Verify all proofs in token have meet the required spend
491    /// Can be used to allow a wallet to accept payments offline while reducing
492    /// the risk of claiming back to the limits let by the spending_conditions
493    #[instrument(skip(self, token))]
494    pub async fn verify_token_p2pk(
495        &self,
496        token: &Token,
497        spending_conditions: SpendingConditions,
498    ) -> Result<(), Error> {
499        let (refund_keys, pubkeys, locktime, num_sigs) = match spending_conditions {
500            SpendingConditions::P2PKConditions { data, conditions } => {
501                let mut pubkeys = vec![data];
502
503                match conditions {
504                    Some(conditions) => {
505                        pubkeys.extend(conditions.pubkeys.unwrap_or_default());
506
507                        (
508                            conditions.refund_keys,
509                            Some(pubkeys),
510                            conditions.locktime,
511                            conditions.num_sigs,
512                        )
513                    }
514                    None => (None, Some(pubkeys), None, None),
515                }
516            }
517            SpendingConditions::HTLCConditions {
518                conditions,
519                data: _,
520            } => match conditions {
521                Some(conditions) => (
522                    conditions.refund_keys,
523                    conditions.pubkeys,
524                    conditions.locktime,
525                    conditions.num_sigs,
526                ),
527                None => (None, None, None, None),
528            },
529        };
530
531        if refund_keys.is_some() && locktime.is_none() {
532            tracing::warn!(
533                "Invalid spending conditions set: Locktime must be set if refund keys are allowed"
534            );
535            return Err(Error::InvalidSpendConditions(
536                "Must set locktime".to_string(),
537            ));
538        }
539        if token.mint_url()? != self.mint_url {
540            return Err(Error::IncorrectWallet(format!(
541                "Should be {} not {}",
542                self.mint_url,
543                token.mint_url()?
544            )));
545        }
546        // We need the keysets information to properly convert from token proof to proof
547        let keysets_info = self.load_mint_keysets().await?;
548        let proofs = token.proofs(&keysets_info)?;
549
550        for proof in proofs {
551            let secret: nut10::Secret = (&proof.secret).try_into()?;
552
553            let proof_conditions: SpendingConditions = secret.try_into()?;
554
555            if num_sigs.ne(&proof_conditions.num_sigs()) {
556                tracing::debug!(
557                    "Spending condition requires: {:?} sigs proof secret specifies: {:?}",
558                    num_sigs,
559                    proof_conditions.num_sigs()
560                );
561
562                return Err(Error::P2PKConditionsNotMet(
563                    "Num sigs did not match spending condition".to_string(),
564                ));
565            }
566
567            let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default();
568            let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default();
569
570            // Check the Proof has the required pubkeys
571            if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
572                || !proof_pubkeys
573                    .iter()
574                    .all(|pubkey| spending_condition_pubkeys.contains(pubkey))
575            {
576                tracing::debug!("Proof did not included Publickeys meeting condition");
577                tracing::debug!("{:?}", proof_pubkeys);
578                tracing::debug!("{:?}", spending_condition_pubkeys);
579                return Err(Error::P2PKConditionsNotMet(
580                    "Pubkeys in proof not allowed by spending condition".to_string(),
581                ));
582            }
583
584            // If spending condition refund keys is allowed (Some(Empty Vec))
585            // If spending conition refund keys is allowed to restricted set of keys check
586            // it is one of them Check that proof locktime is > condition
587            // locktime
588
589            if let Some(proof_refund_keys) = proof_conditions.refund_keys() {
590                let proof_locktime = proof_conditions
591                    .locktime()
592                    .ok_or(Error::LocktimeNotProvided)?;
593
594                if let (Some(condition_refund_keys), Some(condition_locktime)) =
595                    (&refund_keys, locktime)
596                {
597                    // Proof locktime must be greater then condition locktime to ensure it
598                    // cannot be claimed back
599                    if proof_locktime.lt(&condition_locktime) {
600                        return Err(Error::P2PKConditionsNotMet(
601                            "Proof locktime less then required".to_string(),
602                        ));
603                    }
604
605                    // A non empty condition refund key list is used as a restricted set of keys
606                    // returns are allowed to An empty list means the
607                    // proof can be refunded to anykey set in the secret
608                    if !condition_refund_keys.is_empty()
609                        && !proof_refund_keys
610                            .iter()
611                            .all(|refund_key| condition_refund_keys.contains(refund_key))
612                    {
613                        return Err(Error::P2PKConditionsNotMet(
614                            "Refund Key not allowed".to_string(),
615                        ));
616                    }
617                } else {
618                    // Spending conditions does not allow refund keys
619                    return Err(Error::P2PKConditionsNotMet(
620                        "Spending condition does not allow refund keys".to_string(),
621                    ));
622                }
623            }
624        }
625
626        Ok(())
627    }
628
629    /// Verify all proofs in token have a valid DLEQ proof
630    #[instrument(skip(self, token))]
631    pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
632        let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
633
634        // TODO: Get mint url
635        // if mint_url != &self.mint_url {
636        //     return Err(Error::IncorrectWallet(format!(
637        //         "Should be {} not {}",
638        //         self.mint_url, mint_url
639        //     )));
640        // }
641
642        // We need the keysets information to properly convert from token proof to proof
643        let keysets_info = self.load_mint_keysets().await?;
644        let proofs = token.proofs(&keysets_info)?;
645        for proof in proofs {
646            let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
647                Some(keys) => keys.amount_key(proof.amount),
648                None => {
649                    let keys = self.load_keyset_keys(proof.keyset_id).await?;
650
651                    let key = keys.amount_key(proof.amount);
652                    keys_cache.insert(proof.keyset_id, keys);
653
654                    key
655                }
656            }
657            .ok_or(Error::AmountKey)?;
658
659            proof
660                .verify_dleq(mint_pubkey)
661                .map_err(|_| Error::CouldNotVerifyDleq)?;
662        }
663
664        Ok(())
665    }
666}
667
668impl Drop for Wallet {
669    fn drop(&mut self) {
670        self.seed.zeroize();
671    }
672}