1#![doc = include_str!("./README.md")]
2
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::str::FromStr;
6use std::sync::atomic::AtomicBool;
7use std::sync::Arc;
8use std::time::Duration;
9
10use cdk_common::amount::FeeAndAmounts;
11use cdk_common::database::{self, WalletDatabase};
12use cdk_common::parking_lot::RwLock;
13use cdk_common::subscription::WalletParams;
14use getrandom::getrandom;
15use subscription::{ActiveSubscription, SubscriptionManager};
16#[cfg(feature = "auth")]
17use tokio::sync::RwLock as TokioRwLock;
18use tracing::instrument;
19use zeroize::Zeroize;
20
21use crate::amount::SplitTarget;
22use crate::dhke::construct_proofs;
23use crate::error::Error;
24use crate::fees::calculate_fee;
25use crate::mint_url::MintUrl;
26use crate::nuts::nut00::token::Token;
27use crate::nuts::nut17::Kind;
28use crate::nuts::{
29 nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs,
30 RestoreRequest, SpendingConditions, State,
31};
32use crate::types::ProofInfo;
33use crate::util::unix_time;
34use crate::wallet::mint_metadata_cache::MintMetadataCache;
35use crate::Amount;
36#[cfg(feature = "auth")]
37use crate::OidcClient;
38
39#[cfg(feature = "auth")]
40mod auth;
41#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
42pub use mint_connector::TorHttpClient;
43mod balance;
44mod builder;
45mod issue;
46mod keysets;
47mod melt;
48mod mint_connector;
49mod mint_metadata_cache;
50pub mod multi_mint_wallet;
51pub mod payment_request;
52mod proofs;
53mod receive;
54mod reclaim;
55mod send;
56#[cfg(not(target_arch = "wasm32"))]
57mod streams;
58pub mod subscription;
59mod swap;
60mod transactions;
61pub mod util;
62
63#[cfg(feature = "auth")]
64pub use auth::{AuthMintConnector, AuthWallet};
65pub use builder::WalletBuilder;
66pub use cdk_common::wallet as types;
67#[cfg(feature = "auth")]
68pub use mint_connector::http_client::AuthHttpClient as BaseAuthHttpClient;
69pub use mint_connector::http_client::HttpClient as BaseHttpClient;
70pub use mint_connector::transport::Transport as HttpTransport;
71#[cfg(feature = "auth")]
72pub use mint_connector::AuthHttpClient;
73pub use mint_connector::{HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector};
74pub use multi_mint_wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet};
75pub use receive::ReceiveOptions;
76pub use send::{PreparedSend, SendMemo, SendOptions};
77pub use types::{MeltQuote, MintQuote, SendKind};
78
79use crate::nuts::nut00::ProofsMethods;
80
81#[derive(Debug, Clone)]
87pub struct Wallet {
88 pub mint_url: MintUrl,
90 pub unit: CurrencyUnit,
92 pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
94 pub metadata_cache: Arc<MintMetadataCache>,
96 pub target_proof_count: usize,
98 metadata_cache_ttl: Arc<RwLock<Option<Duration>>>,
99 #[cfg(feature = "auth")]
100 auth_wallet: Arc<TokioRwLock<Option<AuthWallet>>>,
101 seed: [u8; 64],
102 client: Arc<dyn MintConnector + Send + Sync>,
103 subscription: SubscriptionManager,
104 in_error_swap_reverted_proofs: Arc<AtomicBool>,
105}
106
107const ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
108
109#[derive(Debug, Clone)]
111pub enum WalletSubscription {
112 ProofState(Vec<String>),
114 Bolt11MintQuoteState(Vec<String>),
116 Bolt11MeltQuoteState(Vec<String>),
118 Bolt12MintQuoteState(Vec<String>),
120}
121
122impl From<WalletSubscription> for WalletParams {
123 fn from(val: WalletSubscription) -> Self {
124 let mut buffer = vec![0u8; 10];
125
126 getrandom(&mut buffer).expect("Failed to generate random bytes");
127
128 let id = Arc::new(
129 buffer
130 .iter()
131 .map(|&byte| {
132 let index = byte as usize % ALPHANUMERIC.len(); ALPHANUMERIC[index] as char
134 })
135 .collect::<String>(),
136 );
137
138 match val {
139 WalletSubscription::ProofState(filters) => WalletParams {
140 filters,
141 kind: Kind::ProofState,
142 id,
143 },
144 WalletSubscription::Bolt11MintQuoteState(filters) => WalletParams {
145 filters,
146 kind: Kind::Bolt11MintQuote,
147 id,
148 },
149 WalletSubscription::Bolt11MeltQuoteState(filters) => WalletParams {
150 filters,
151 kind: Kind::Bolt11MeltQuote,
152 id,
153 },
154 WalletSubscription::Bolt12MintQuoteState(filters) => WalletParams {
155 filters,
156 kind: Kind::Bolt12MintQuote,
157 id,
158 },
159 }
160 }
161}
162
163impl Wallet {
164 pub fn new(
191 mint_url: &str,
192 unit: CurrencyUnit,
193 localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
194 seed: [u8; 64],
195 target_proof_count: Option<usize>,
196 ) -> Result<Self, Error> {
197 let mint_url = MintUrl::from_str(mint_url)?;
198
199 WalletBuilder::new()
200 .mint_url(mint_url)
201 .unit(unit)
202 .localstore(localstore)
203 .seed(seed)
204 .target_proof_count(target_proof_count.unwrap_or(3))
205 .build()
206 }
207
208 pub async fn subscribe<T: Into<WalletParams>>(&self, query: T) -> ActiveSubscription {
210 self.subscription
211 .subscribe(self.mint_url.clone(), query.into())
212 .expect("FIXME")
213 }
214
215 #[instrument(skip_all)]
217 pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
218 let proofs_per_keyset = proofs.count_by_keyset();
219 self.get_proofs_fee_by_count(proofs_per_keyset).await
220 }
221
222 pub async fn get_proofs_fee_by_count(
224 &self,
225 proofs_per_keyset: HashMap<Id, u64>,
226 ) -> Result<Amount, Error> {
227 let mut fee_per_keyset = HashMap::new();
228 let metadata = self
229 .metadata_cache
230 .load(&self.localstore, &self.client, {
231 let ttl = self.metadata_cache_ttl.read();
232 *ttl
233 })
234 .await?;
235
236 for keyset_id in proofs_per_keyset.keys() {
237 let mint_keyset_info = metadata
238 .keysets
239 .get(keyset_id)
240 .ok_or(Error::UnknownKeySet)?;
241 fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
242 }
243
244 let fee = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
245
246 Ok(fee)
247 }
248
249 #[instrument(skip_all)]
251 pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
252 let input_fee_ppk = self
253 .metadata_cache
254 .load(&self.localstore, &self.client, {
255 let ttl = self.metadata_cache_ttl.read();
256 *ttl
257 })
258 .await?
259 .keysets
260 .get(keyset_id)
261 .ok_or(Error::UnknownKeySet)?
262 .input_fee_ppk;
263
264 let fee = (input_fee_ppk * count).div_ceil(1000);
265
266 Ok(Amount::from(fee))
267 }
268
269 #[instrument(skip(self))]
272 pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
273 self.localstore
275 .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
276 .await?;
277
278 self.mint_url = new_mint_url;
280
281 Ok(())
282 }
283
284 #[instrument(skip(self))]
286 pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
287 let mint_info = self
288 .metadata_cache
289 .load_from_mint(&self.localstore, &self.client)
290 .await?
291 .mint_info
292 .clone();
293
294 if let Some(mint_unix_time) = mint_info.time {
296 let current_unix_time = unix_time();
297 if current_unix_time.abs_diff(mint_unix_time) > 30 {
298 tracing::warn!(
299 "Mint time does match wallet time. Mint: {}, Wallet: {}",
300 mint_unix_time,
301 current_unix_time
302 );
303 return Err(Error::MintTimeExceedsTolerance);
304 }
305 }
306
307 #[cfg(feature = "auth")]
309 {
310 let mut auth_wallet = self.auth_wallet.write().await;
311 match &*auth_wallet {
312 Some(auth_wallet) => {
313 let mut protected_endpoints = auth_wallet.protected_endpoints.write().await;
314 *protected_endpoints = mint_info.protected_endpoints();
315
316 if let Some(oidc_client) = mint_info
317 .openid_discovery()
318 .map(|url| OidcClient::new(url, None))
319 {
320 auth_wallet.set_oidc_client(Some(oidc_client)).await;
321 }
322 }
323 None => {
324 tracing::info!("Mint has auth enabled creating auth wallet");
325
326 let oidc_client = mint_info
327 .openid_discovery()
328 .map(|url| OidcClient::new(url, None));
329 let new_auth_wallet = AuthWallet::new(
330 self.mint_url.clone(),
331 None,
332 self.localstore.clone(),
333 self.metadata_cache.clone(),
334 mint_info.protected_endpoints(),
335 oidc_client,
336 );
337 *auth_wallet = Some(new_auth_wallet.clone());
338
339 self.client.set_auth_wallet(Some(new_auth_wallet)).await;
340 }
341 }
342 }
343
344 tracing::trace!("Mint info updated for {}", self.mint_url);
345
346 Ok(Some(mint_info))
347 }
348
349 #[instrument(skip(self))]
355 pub async fn load_mint_info(&self) -> Result<MintInfo, Error> {
356 let mint_info = self
357 .metadata_cache
358 .load(&self.localstore, &self.client, {
359 let ttl = self.metadata_cache_ttl.read();
360 *ttl
361 })
362 .await?
363 .mint_info
364 .clone();
365
366 Ok(mint_info)
367 }
368
369 #[instrument(skip(self))]
371 pub async fn amounts_needed_for_state_target(
372 &self,
373 fee_and_amounts: &FeeAndAmounts,
374 ) -> Result<Vec<Amount>, Error> {
375 let unspent_proofs = self.get_unspent_proofs().await?;
376
377 let amounts_count: HashMap<u64, u64> =
378 unspent_proofs
379 .iter()
380 .fold(HashMap::new(), |mut acc, proof| {
381 let amount = proof.amount;
382 let counter = acc.entry(u64::from(amount)).or_insert(0);
383 *counter += 1;
384 acc
385 });
386
387 let needed_amounts =
388 fee_and_amounts
389 .amounts()
390 .iter()
391 .fold(Vec::new(), |mut acc, amount| {
392 let count_needed = (self.target_proof_count as u64)
393 .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
394
395 for _i in 0..count_needed {
396 acc.push(Amount::from(*amount));
397 }
398
399 acc
400 });
401 Ok(needed_amounts)
402 }
403
404 #[instrument(skip(self))]
406 async fn determine_split_target_values(
407 &self,
408 change_amount: Amount,
409 fee_and_amounts: &FeeAndAmounts,
410 ) -> Result<SplitTarget, Error> {
411 let mut amounts_needed_refill = self
412 .amounts_needed_for_state_target(fee_and_amounts)
413 .await?;
414
415 amounts_needed_refill.sort();
416
417 let mut values = Vec::new();
418
419 for amount in amounts_needed_refill {
420 let values_sum = Amount::try_sum(values.clone().into_iter())?;
421 if values_sum + amount <= change_amount {
422 values.push(amount);
423 }
424 }
425
426 Ok(SplitTarget::Values(values))
427 }
428
429 #[instrument(skip(self))]
431 pub async fn restore(&self) -> Result<Amount, Error> {
432 if self
434 .localstore
435 .get_mint(self.mint_url.clone())
436 .await?
437 .is_none()
438 {
439 self.fetch_mint_info().await?;
440 }
441
442 let keysets = self.load_mint_keysets().await?;
443
444 let mut restored_value = Amount::ZERO;
445
446 for keyset in keysets {
447 let keys = self.load_keyset_keys(keyset.id).await?;
448 let mut empty_batch = 0;
449 let mut start_counter = 0;
450
451 while empty_batch.lt(&3) {
452 let premint_secrets = PreMintSecrets::restore_batch(
453 keyset.id,
454 &self.seed,
455 start_counter,
456 start_counter + 100,
457 )?;
458
459 tracing::debug!(
460 "Attempting to restore counter {}-{} for mint {} keyset {}",
461 start_counter,
462 start_counter + 100,
463 self.mint_url,
464 keyset.id
465 );
466
467 let restore_request = RestoreRequest {
468 outputs: premint_secrets.blinded_messages(),
469 };
470
471 let response = self.client.post_restore(restore_request).await?;
472
473 if response.signatures.is_empty() {
474 empty_batch += 1;
475 start_counter += 100;
476 continue;
477 }
478
479 let premint_secrets: Vec<_> = premint_secrets
480 .secrets
481 .iter()
482 .filter(|p| response.outputs.contains(&p.blinded_message))
483 .collect();
484
485 assert_eq!(response.outputs.len(), premint_secrets.len());
488
489 let proofs = construct_proofs(
490 response.signatures,
491 premint_secrets.iter().map(|p| p.r.clone()).collect(),
492 premint_secrets.iter().map(|p| p.secret.clone()).collect(),
493 &keys,
494 )?;
495
496 tracing::debug!("Restored {} proofs", proofs.len());
497
498 self.localstore
499 .increment_keyset_counter(&keyset.id, proofs.len() as u32)
500 .await?;
501
502 let states = self.check_proofs_spent(proofs.clone()).await?;
503
504 let unspent_proofs: Vec<Proof> = proofs
505 .iter()
506 .zip(states)
507 .filter(|(_, state)| !state.state.eq(&State::Spent))
508 .map(|(p, _)| p)
509 .cloned()
510 .collect();
511
512 restored_value += unspent_proofs.total_amount()?;
513
514 let unspent_proofs = unspent_proofs
515 .into_iter()
516 .map(|proof| {
517 ProofInfo::new(
518 proof,
519 self.mint_url.clone(),
520 State::Unspent,
521 keyset.unit.clone(),
522 )
523 })
524 .collect::<Result<Vec<ProofInfo>, _>>()?;
525
526 self.localstore
527 .update_proofs(unspent_proofs, vec![])
528 .await?;
529
530 empty_batch = 0;
531 start_counter += 100;
532 }
533 }
534 Ok(restored_value)
535 }
536
537 #[instrument(skip(self, token))]
541 pub async fn verify_token_p2pk(
542 &self,
543 token: &Token,
544 spending_conditions: SpendingConditions,
545 ) -> Result<(), Error> {
546 let (refund_keys, pubkeys, locktime, num_sigs) = match spending_conditions {
547 SpendingConditions::P2PKConditions { data, conditions } => {
548 let mut pubkeys = vec![data];
549
550 match conditions {
551 Some(conditions) => {
552 pubkeys.extend(conditions.pubkeys.unwrap_or_default());
553
554 (
555 conditions.refund_keys,
556 Some(pubkeys),
557 conditions.locktime,
558 conditions.num_sigs,
559 )
560 }
561 None => (None, Some(pubkeys), None, None),
562 }
563 }
564 SpendingConditions::HTLCConditions {
565 conditions,
566 data: _,
567 } => match conditions {
568 Some(conditions) => (
569 conditions.refund_keys,
570 conditions.pubkeys,
571 conditions.locktime,
572 conditions.num_sigs,
573 ),
574 None => (None, None, None, None),
575 },
576 };
577
578 if refund_keys.is_some() && locktime.is_none() {
579 tracing::warn!(
580 "Invalid spending conditions set: Locktime must be set if refund keys are allowed"
581 );
582 return Err(Error::InvalidSpendConditions(
583 "Must set locktime".to_string(),
584 ));
585 }
586 if token.mint_url()? != self.mint_url {
587 return Err(Error::IncorrectWallet(format!(
588 "Should be {} not {}",
589 self.mint_url,
590 token.mint_url()?
591 )));
592 }
593 let keysets_info = self.load_mint_keysets().await?;
595 let proofs = token.proofs(&keysets_info)?;
596
597 for proof in proofs {
598 let secret: nut10::Secret = (&proof.secret).try_into()?;
599
600 let proof_conditions: SpendingConditions = secret.try_into()?;
601
602 if num_sigs.ne(&proof_conditions.num_sigs()) {
603 tracing::debug!(
604 "Spending condition requires: {:?} sigs proof secret specifies: {:?}",
605 num_sigs,
606 proof_conditions.num_sigs()
607 );
608
609 return Err(Error::P2PKConditionsNotMet(
610 "Num sigs did not match spending condition".to_string(),
611 ));
612 }
613
614 let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default();
615 let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default();
616
617 if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
619 || !proof_pubkeys
620 .iter()
621 .all(|pubkey| spending_condition_pubkeys.contains(pubkey))
622 {
623 tracing::debug!("Proof did not included Publickeys meeting condition");
624 tracing::debug!("{:?}", proof_pubkeys);
625 tracing::debug!("{:?}", spending_condition_pubkeys);
626 return Err(Error::P2PKConditionsNotMet(
627 "Pubkeys in proof not allowed by spending condition".to_string(),
628 ));
629 }
630
631 if let Some(proof_refund_keys) = proof_conditions.refund_keys() {
637 let proof_locktime = proof_conditions
638 .locktime()
639 .ok_or(Error::LocktimeNotProvided)?;
640
641 if let (Some(condition_refund_keys), Some(condition_locktime)) =
642 (&refund_keys, locktime)
643 {
644 if proof_locktime.lt(&condition_locktime) {
647 return Err(Error::P2PKConditionsNotMet(
648 "Proof locktime less then required".to_string(),
649 ));
650 }
651
652 if !condition_refund_keys.is_empty()
656 && !proof_refund_keys
657 .iter()
658 .all(|refund_key| condition_refund_keys.contains(refund_key))
659 {
660 return Err(Error::P2PKConditionsNotMet(
661 "Refund Key not allowed".to_string(),
662 ));
663 }
664 } else {
665 return Err(Error::P2PKConditionsNotMet(
667 "Spending condition does not allow refund keys".to_string(),
668 ));
669 }
670 }
671 }
672
673 Ok(())
674 }
675
676 #[instrument(skip(self, token))]
678 pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
679 let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
680
681 let keysets_info = self.load_mint_keysets().await?;
691 let proofs = token.proofs(&keysets_info)?;
692 for proof in proofs {
693 let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
694 Some(keys) => keys.amount_key(proof.amount),
695 None => {
696 let keys = self.load_keyset_keys(proof.keyset_id).await?;
697
698 let key = keys.amount_key(proof.amount);
699 keys_cache.insert(proof.keyset_id, keys);
700
701 key
702 }
703 }
704 .ok_or(Error::AmountKey)?;
705
706 proof
707 .verify_dleq(mint_pubkey)
708 .map_err(|_| Error::CouldNotVerifyDleq)?;
709 }
710
711 Ok(())
712 }
713
714 pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
718 self.client = client;
719 }
720
721 pub fn set_target_proof_count(&mut self, count: usize) {
725 self.target_proof_count = count;
726 }
727}
728
729impl Drop for Wallet {
730 fn drop(&mut self) {
731 self.seed.zeroize();
732 }
733}