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#[derive(Debug, Clone)]
77pub struct Wallet {
78 pub mint_url: MintUrl,
80 pub unit: CurrencyUnit,
82 pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
84 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#[derive(Debug, Clone)]
97pub enum WalletSubscription {
98 ProofState(Vec<String>),
100 Bolt11MintQuoteState(Vec<String>),
102 Bolt11MeltQuoteState(Vec<String>),
104 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(); 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 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 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 #[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 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 #[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 #[instrument(skip(self))]
245 pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
246 self.localstore
248 .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
249 .await?;
250
251 self.mint_url = new_mint_url;
253
254 Ok(())
255 }
256
257 #[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 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 #[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 #[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 #[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 #[instrument(skip(self))]
384 pub async fn restore(&self) -> Result<Amount, Error> {
385 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 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 #[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 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 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 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 if proof_locktime.lt(&condition_locktime) {
600 return Err(Error::P2PKConditionsNotMet(
601 "Proof locktime less then required".to_string(),
602 ));
603 }
604
605 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 return Err(Error::P2PKConditionsNotMet(
620 "Spending condition does not allow refund keys".to_string(),
621 ));
622 }
623 }
624 }
625
626 Ok(())
627 }
628
629 #[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 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}