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#[derive(Debug, Clone)]
118pub struct Wallet {
119 pub mint_url: MintUrl,
121 pub unit: CurrencyUnit,
123 pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
125 pub metadata_cache: Arc<MintMetadataCache>,
127 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#[derive(Debug, Clone)]
142pub enum WalletSubscription {
143 ProofState(Vec<String>),
145 Bolt11MintQuoteState(Vec<String>),
147 Bolt11MeltQuoteState(Vec<String>),
149 Bolt12MeltQuoteState(Vec<String>),
151 Bolt12MintQuoteState(Vec<String>),
153 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(); 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 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 {
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 #[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 #[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 #[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 #[instrument(skip(self))]
518 pub async fn restore(&self) -> Result<Restored, Error> {
519 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 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 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 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 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 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 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 #[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 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 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 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 if proof_locktime.lt(&condition_locktime) {
793 return Err(Error::P2PKConditionsNotMet(
794 "Proof locktime less then required".to_string(),
795 ));
796 }
797
798 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 return Err(Error::P2PKConditionsNotMet(
813 "Spending condition does not allow refund keys".to_string(),
814 ));
815 }
816 }
817 }
818
819 Ok(())
820 }
821
822 #[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 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 pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
864 self.client = client;
865 }
866
867 pub fn mint_connector(&self) -> Arc<dyn MintConnector + Send + Sync> {
869 self.client.clone()
870 }
871
872 pub fn set_target_proof_count(&mut self, count: usize) {
876 self.target_proof_count = count;
877 }
878
879 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 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 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 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 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]
960 fn test_restore_signature_matching_in_order() {
961 let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
963
964 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 let sig1 = BlindSignature {
999 amount: Amount::from(1),
1000 keyset_id,
1001 c: blinded1, 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 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 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 assert_eq!(matched_secrets.len(), 3);
1045
1046 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 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]
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 let response_outputs = [
1116 premint3.blinded_message.clone(), premint1.blinded_message.clone(), premint2.blinded_message.clone(), ];
1120 let response_signatures = [sig3.clone(), sig1.clone(), sig2.clone()];
1121
1122 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 assert_eq!(matched_secrets.len(), 3);
1142
1143 assert_eq!(matched_secrets[0].0, 0); assert_eq!(matched_secrets[0].2.amount, Amount::from(1)); assert_eq!(matched_secrets[1].0, 1); assert_eq!(matched_secrets[1].2.amount, Amount::from(2)); assert_eq!(matched_secrets[2].0, 2); assert_eq!(matched_secrets[2].2.amount, Amount::from(4)); }
1155
1156 #[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 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 assert_eq!(matched_secrets.len(), 2);
1233
1234 assert_eq!(matched_secrets[0].0, 0); assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
1237
1238 assert_eq!(matched_secrets[1].0, 2); assert_eq!(matched_secrets[1].2.amount, Amount::from(4));
1240 }
1241}