1use std::collections::HashMap;
4use std::fmt;
5use std::str::FromStr;
6
7use async_trait::async_trait;
8use bitcoin::bip32::DerivationPath;
9use bitcoin::hashes::{sha256, Hash, HashEngine};
10use cashu::amount::{FeeAndAmounts, KeysetFeeAndAmounts, SplitTarget};
11use cashu::nuts::nut07::ProofState;
12use cashu::nuts::nut18::PaymentRequest;
13use cashu::nuts::{AuthProof, Keys};
14use cashu::util::hex;
15use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use crate::mint_quote::quote_state_from_amounts;
20use crate::mint_url::MintUrl;
21use crate::nuts::{
22 CurrencyUnit, Id, MeltQuoteState, MintQuoteState, SecretKey, SpendingConditions, State,
23};
24use crate::{Amount, Error};
25
26pub mod saga;
27
28pub use saga::{
29 IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
30 ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
31 SwapSagaState, WalletSaga, WalletSagaState,
32};
33
34#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36pub struct WalletKey {
37 pub mint_url: MintUrl,
39 pub unit: CurrencyUnit,
41}
42
43impl fmt::Display for WalletKey {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
46 }
47}
48
49impl WalletKey {
50 pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
52 Self { mint_url, unit }
53 }
54}
55
56#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ProofInfo {
59 pub proof: Proof,
61 pub y: PublicKey,
63 pub mint_url: MintUrl,
65 pub state: State,
67 pub spending_condition: Option<SpendingConditions>,
69 pub unit: CurrencyUnit,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub used_by_operation: Option<Uuid>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub created_by_operation: Option<Uuid>,
77}
78
79impl ProofInfo {
80 pub fn new(
82 proof: Proof,
83 mint_url: MintUrl,
84 state: State,
85 unit: CurrencyUnit,
86 ) -> Result<Self, Error> {
87 let y = proof.y()?;
88
89 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
90
91 Ok(Self {
92 proof,
93 y,
94 mint_url,
95 state,
96 spending_condition,
97 unit,
98 used_by_operation: None,
99 created_by_operation: None,
100 })
101 }
102
103 pub fn new_with_operations(
105 proof: Proof,
106 mint_url: MintUrl,
107 state: State,
108 unit: CurrencyUnit,
109 used_by_operation: Option<Uuid>,
110 created_by_operation: Option<Uuid>,
111 ) -> Result<Self, Error> {
112 let y = proof.y()?;
113
114 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
115
116 Ok(Self {
117 proof,
118 y,
119 mint_url,
120 state,
121 spending_condition,
122 unit,
123 used_by_operation,
124 created_by_operation,
125 })
126 }
127
128 pub fn matches_conditions(
130 &self,
131 mint_url: &Option<MintUrl>,
132 unit: &Option<CurrencyUnit>,
133 state: &Option<Vec<State>>,
134 spending_conditions: &Option<Vec<SpendingConditions>>,
135 ) -> bool {
136 if let Some(mint_url) = mint_url {
137 if mint_url.ne(&self.mint_url) {
138 return false;
139 }
140 }
141
142 if let Some(unit) = unit {
143 if unit.ne(&self.unit) {
144 return false;
145 }
146 }
147
148 if let Some(state) = state {
149 if !state.contains(&self.state) {
150 return false;
151 }
152 }
153
154 if let Some(spending_conditions) = spending_conditions {
155 match &self.spending_condition {
156 None => {
157 if !spending_conditions.is_empty() {
158 return false;
159 }
160 }
161 Some(s) => {
162 if !spending_conditions.contains(s) {
163 return false;
164 }
165 }
166 }
167 }
168
169 true
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct MintQuote {
176 pub id: String,
178 pub mint_url: MintUrl,
180 pub payment_method: PaymentMethod,
182 pub amount: Option<Amount>,
187 pub unit: CurrencyUnit,
189 pub request: String,
191 pub state: MintQuoteState,
193 pub expiry: u64,
195 pub secret_key: Option<SecretKey>,
197 #[serde(default)]
199 pub amount_issued: Amount,
200 #[serde(default)]
202 pub amount_paid: Amount,
203 pub estimated_blocks: Option<u32>,
205 #[serde(default)]
207 pub used_by_operation: Option<String>,
208 #[serde(default)]
210 pub version: u32,
211}
212
213#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
215pub struct MeltQuote {
216 pub id: String,
218 pub mint_url: Option<MintUrl>,
220 pub unit: CurrencyUnit,
222 pub amount: Amount,
224 pub request: String,
226 pub fee_reserve: Amount,
228 pub state: MeltQuoteState,
230 pub expiry: u64,
232 #[serde(alias = "payment_preimage")]
234 pub payment_proof: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub estimated_blocks: Option<u32>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub fee_index: Option<u32>,
241 pub payment_method: PaymentMethod,
243 #[serde(default)]
245 pub used_by_operation: Option<String>,
246 #[serde(default)]
248 pub version: u32,
249}
250
251impl MintQuote {
252 #[allow(clippy::too_many_arguments)]
254 pub fn new(
255 id: String,
256 mint_url: MintUrl,
257 payment_method: PaymentMethod,
258 amount: Option<Amount>,
259 unit: CurrencyUnit,
260 request: String,
261 expiry: u64,
262 secret_key: Option<SecretKey>,
263 ) -> Self {
264 Self {
265 id,
266 mint_url,
267 payment_method,
268 amount,
269 unit,
270 request,
271 state: MintQuoteState::Unpaid,
272 expiry,
273 secret_key,
274 amount_issued: Amount::ZERO,
275 amount_paid: Amount::ZERO,
276 estimated_blocks: None,
277 used_by_operation: None,
278 version: 0,
279 }
280 }
281
282 pub fn total_amount(&self) -> Amount {
284 self.amount_paid
285 }
286
287 pub fn state_from_amounts(&self) -> MintQuoteState {
289 quote_state_from_amounts(self.amount_paid, self.amount_issued)
290 }
291
292 pub fn update_state_from_amounts(&mut self) {
294 self.state = self.state_from_amounts();
295 }
296
297 pub fn is_expired(&self, current_time: u64) -> bool {
299 current_time > self.expiry
300 }
301
302 pub fn amount_mintable(&self) -> Amount {
304 if self.payment_method == PaymentMethod::BOLT11 {
305 if self.state == MintQuoteState::Paid {
307 self.amount.unwrap_or(Amount::ZERO)
308 } else {
309 Amount::ZERO
310 }
311 } else {
312 self.amount_paid
314 .checked_sub(self.amount_issued)
315 .unwrap_or(Amount::ZERO)
316 }
317 }
318}
319
320#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
322pub struct Restored {
323 pub spent: Amount,
325 pub unspent: Amount,
327 pub pending: Amount,
329}
330
331#[derive(Debug, Clone)]
339pub struct NUT13Options {
340 pub batch_size: u32,
342 pub max_gap: u32,
344}
345
346impl Default for NUT13Options {
347 fn default() -> Self {
348 Self {
349 batch_size: Self::DEFAULT_BATCH_SIZE,
350 max_gap: Self::DEFAULT_MAX_GAP,
351 }
352 }
353}
354
355impl NUT13Options {
356 pub const DEFAULT_BATCH_SIZE: u32 = 100;
358
359 pub const DEFAULT_MAX_GAP: u32 = 3;
361
362 pub fn new(batch_size: u32, max_gap: u32) -> Result<Self, Error> {
364 let opts = Self {
365 batch_size,
366 max_gap,
367 };
368 opts.validate()?;
369 Ok(opts)
370 }
371
372 pub(crate) fn validate(&self) -> Result<(), Error> {
373 if self.batch_size == 0 {
374 return Err(Error::InvalidNut13Options {
375 field: "batch_size",
376 reason: "must be greater than zero",
377 });
378 }
379
380 if self.max_gap == 0 {
381 return Err(Error::InvalidNut13Options {
382 field: "max_gap",
383 reason: "must be greater than zero",
384 });
385 }
386
387 Ok(())
388 }
389}
390
391#[derive(Clone, Default)]
393pub struct SendOptions {
394 pub memo: Option<SendMemo>,
396 pub conditions: Option<SpendingConditions>,
398 pub amount_split_target: SplitTarget,
400 pub send_kind: SendKind,
402 pub include_fee: bool,
404 pub max_proofs: Option<usize>,
406 pub metadata: HashMap<String, String>,
408 pub use_p2bk: bool,
410 pub p2pk_signing_keys: Vec<SecretKey>,
412 pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
414}
415
416impl fmt::Debug for SendOptions {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 f.debug_struct("SendOptions")
419 .field("memo", &self.memo)
420 .field("conditions", &self.conditions)
421 .field("amount_split_target", &self.amount_split_target)
422 .field("send_kind", &self.send_kind)
423 .field("include_fee", &self.include_fee)
424 .field("max_proofs", &self.max_proofs)
425 .field("metadata", &self.metadata)
426 .field("use_p2bk", &self.use_p2bk)
427 .field("p2pk_signing_keys", &"[redacted]")
428 .field(
429 "p2pk_locked_proof_send_mode",
430 &self.p2pk_locked_proof_send_mode,
431 )
432 .finish()
433 }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
438pub enum P2PKLockedProofSendMode {
439 #[default]
441 Swap,
442 SignAndSend,
444}
445
446#[derive(Debug, Clone)]
448pub struct SendMemo {
449 pub memo: String,
451 pub include_memo: bool,
453}
454
455impl SendMemo {
456 pub fn for_token(memo: &str) -> Self {
458 Self {
459 memo: memo.to_string(),
460 include_memo: true,
461 }
462 }
463}
464
465#[derive(Clone, Default)]
467pub struct ReceiveOptions {
468 pub amount_split_target: SplitTarget,
470 pub p2pk_signing_keys: Vec<SecretKey>,
472 pub preimages: Vec<String>,
474 pub metadata: HashMap<String, String>,
476}
477
478impl fmt::Debug for ReceiveOptions {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 f.debug_struct("ReceiveOptions")
481 .field("amount_split_target", &self.amount_split_target)
482 .field("p2pk_signing_keys", &"[redacted]")
483 .field("preimages", &self.preimages)
484 .field("metadata", &self.metadata)
485 .finish()
486 }
487}
488
489#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
491pub enum SendKind {
492 #[default]
493 OnlineExact,
495 OnlineTolerance(Amount),
497 OfflineExact,
499 OfflineTolerance(Amount),
501}
502
503impl SendKind {
504 pub fn is_online(&self) -> bool {
506 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
507 }
508
509 pub fn is_offline(&self) -> bool {
511 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
512 }
513
514 pub fn is_exact(&self) -> bool {
516 matches!(self, Self::OnlineExact | Self::OfflineExact)
517 }
518
519 pub fn has_tolerance(&self) -> bool {
521 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
527pub struct Transaction {
528 pub mint_url: MintUrl,
530 pub direction: TransactionDirection,
532 pub amount: Amount,
534 pub fee: Amount,
536 pub unit: CurrencyUnit,
538 pub ys: Vec<PublicKey>,
540 pub timestamp: u64,
542 pub memo: Option<String>,
544 pub metadata: HashMap<String, String>,
546 pub quote_id: Option<String>,
548 pub payment_request: Option<String>,
550 #[serde(alias = "payment_preimage")]
552 pub payment_proof: Option<String>,
553 #[serde(default)]
555 pub payment_method: Option<PaymentMethod>,
556 #[serde(default)]
558 pub saga_id: Option<Uuid>,
559}
560
561impl Transaction {
562 pub fn id(&self) -> TransactionId {
564 TransactionId::new(self.ys.clone())
565 }
566
567 pub fn matches_conditions(
569 &self,
570 mint_url: &Option<MintUrl>,
571 direction: &Option<TransactionDirection>,
572 unit: &Option<CurrencyUnit>,
573 ) -> bool {
574 if let Some(mint_url) = mint_url {
575 if &self.mint_url != mint_url {
576 return false;
577 }
578 }
579 if let Some(direction) = direction {
580 if &self.direction != direction {
581 return false;
582 }
583 }
584 if let Some(unit) = unit {
585 if &self.unit != unit {
586 return false;
587 }
588 }
589 true
590 }
591}
592
593impl PartialOrd for Transaction {
594 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
595 Some(self.cmp(other))
596 }
597}
598
599impl Ord for Transaction {
600 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
601 self.timestamp
602 .cmp(&other.timestamp)
603 .reverse()
604 .then_with(|| self.id().cmp(&other.id()))
605 }
606}
607
608#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
610pub enum TransactionDirection {
611 Incoming,
613 Outgoing,
615}
616
617impl std::fmt::Display for TransactionDirection {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 match self {
620 TransactionDirection::Incoming => write!(f, "Incoming"),
621 TransactionDirection::Outgoing => write!(f, "Outgoing"),
622 }
623 }
624}
625
626impl FromStr for TransactionDirection {
627 type Err = Error;
628
629 fn from_str(value: &str) -> Result<Self, Self::Err> {
630 match value {
631 "Incoming" => Ok(Self::Incoming),
632 "Outgoing" => Ok(Self::Outgoing),
633 _ => Err(Error::InvalidTransactionDirection),
634 }
635 }
636}
637
638#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
640#[serde(transparent)]
641pub struct TransactionId([u8; 32]);
642
643impl TransactionId {
644 pub fn new(ys: Vec<PublicKey>) -> Self {
646 let mut ys = ys;
647 ys.sort();
648 let mut hasher = sha256::Hash::engine();
649 for y in ys {
650 hasher.input(&y.to_bytes());
651 }
652 let hash = sha256::Hash::from_engine(hasher);
653 Self(hash.to_byte_array())
654 }
655
656 pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
658 let ys = proofs
659 .iter()
660 .map(|proof| proof.y())
661 .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
662 Ok(Self::new(ys))
663 }
664
665 pub fn from_bytes(bytes: [u8; 32]) -> Self {
667 Self(bytes)
668 }
669
670 pub fn from_hex(value: &str) -> Result<Self, Error> {
672 let bytes = hex::decode(value)?;
673 if bytes.len() != 32 {
674 return Err(Error::InvalidTransactionId);
675 }
676 let mut array = [0u8; 32];
677 array.copy_from_slice(&bytes);
678 Ok(Self(array))
679 }
680
681 pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
683 if slice.len() != 32 {
684 return Err(Error::InvalidTransactionId);
685 }
686 let mut array = [0u8; 32];
687 array.copy_from_slice(slice);
688 Ok(Self(array))
689 }
690
691 pub fn as_bytes(&self) -> &[u8; 32] {
693 &self.0
694 }
695
696 pub fn as_slice(&self) -> &[u8] {
698 &self.0
699 }
700}
701
702impl std::fmt::Display for TransactionId {
703 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
704 write!(f, "{}", hex::encode(self.0))
705 }
706}
707
708impl FromStr for TransactionId {
709 type Err = Error;
710
711 fn from_str(value: &str) -> Result<Self, Self::Err> {
712 Self::from_hex(value)
713 }
714}
715
716impl TryFrom<Proofs> for TransactionId {
717 type Error = nut00::Error;
718
719 fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
720 Self::from_proofs(proofs)
721 }
722}
723
724#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "snake_case")]
727pub enum OperationKind {
728 Send,
730 Receive,
732 Swap,
734 Mint,
736 Melt,
738}
739
740impl fmt::Display for OperationKind {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 match self {
743 OperationKind::Send => write!(f, "send"),
744 OperationKind::Receive => write!(f, "receive"),
745 OperationKind::Swap => write!(f, "swap"),
746 OperationKind::Mint => write!(f, "mint"),
747 OperationKind::Melt => write!(f, "melt"),
748 }
749 }
750}
751
752impl FromStr for OperationKind {
753 type Err = Error;
754
755 fn from_str(s: &str) -> Result<Self, Self::Err> {
756 match s {
757 "send" => Ok(OperationKind::Send),
758 "receive" => Ok(OperationKind::Receive),
759 "swap" => Ok(OperationKind::Swap),
760 "mint" => Ok(OperationKind::Mint),
761 "melt" => Ok(OperationKind::Melt),
762 _ => Err(Error::InvalidOperationKind),
763 }
764 }
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub enum KeysetFilter {
770 Active,
772 All,
774}
775
776#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
785#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
786pub trait Wallet: Send + Sync {
787 type Error: std::error::Error + Send + Sync + 'static;
789 type Amount: Clone + Send + Sync;
791 type MintUrl: Clone + Send + Sync;
793 type CurrencyUnit: Clone + Send + Sync;
795 type MintInfo: Clone + Send + Sync;
797 type KeySetInfo: Clone + Send + Sync;
799 type MintQuote: Clone + Send + Sync;
801 type MeltQuote: Clone + Send + Sync;
803 type PaymentMethod: Clone + Send + Sync;
805 type MeltOptions: Clone + Send + Sync;
807 type OperationId: Clone + Send + Sync;
809 type PreparedSend<'a>: Send + Sync
811 where
812 Self: 'a;
813 type PreparedMelt<'a>: Send + Sync
815 where
816 Self: 'a;
817 type Subscription: Send + Sync;
819 type SubscribeParams: Clone + Send + Sync;
821 type RecoveryReport: Clone + Send + Sync;
823
824 fn mint_url(&self) -> Self::MintUrl;
826
827 fn unit(&self) -> Self::CurrencyUnit;
829
830 async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
832
833 async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
835
836 async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
838
839 async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
841
842 async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
844
845 async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
847
848 async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
850
851 async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Self::Error>;
853
854 async fn get_mint_keysets(
856 &self,
857 filter: KeysetFilter,
858 ) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
859
860 async fn load_mint_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error> {
862 self.get_mint_keysets(KeysetFilter::Active).await
863 }
864
865 async fn fetch_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
867
868 async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
870
871 async fn get_keyset_count_fee(
873 &self,
874 keyset_id: &Id,
875 count: u64,
876 ) -> Result<Self::Amount, Self::Error>;
877
878 async fn get_keyset_fees_and_amounts_by_id(
880 &self,
881 keyset_id: Id,
882 ) -> Result<FeeAndAmounts, Self::Error>;
883
884 async fn mint_quote(
886 &self,
887 method: Self::PaymentMethod,
888 amount: Option<Self::Amount>,
889 description: Option<String>,
890 extra: Option<String>,
891 ) -> Result<Self::MintQuote, Self::Error>;
892
893 async fn melt_quote(
895 &self,
896 method: Self::PaymentMethod,
897 request: String,
898 options: Option<Self::MeltOptions>,
899 extra: Option<String>,
900 ) -> Result<Self::MeltQuote, Self::Error>;
901
902 async fn list_transactions(
904 &self,
905 direction: Option<TransactionDirection>,
906 ) -> Result<Vec<Transaction>, Self::Error>;
907
908 async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
910
911 async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
913
914 async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
916
917 async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
919
920 async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
922
923 async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
925
926 async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
928
929 async fn calculate_fee(
931 &self,
932 proof_count: u64,
933 keyset_id: Id,
934 ) -> Result<Self::Amount, Self::Error>;
935
936 async fn receive(
938 &self,
939 encoded_token: &str,
940 options: ReceiveOptions,
941 ) -> Result<Self::Amount, Self::Error>;
942
943 async fn receive_proofs(
945 &self,
946 proofs: Proofs,
947 options: ReceiveOptions,
948 memo: Option<String>,
949 token: Option<String>,
950 ) -> Result<Self::Amount, Self::Error>;
951
952 async fn prepare_send(
954 &self,
955 amount: Self::Amount,
956 options: SendOptions,
957 ) -> Result<Self::PreparedSend<'_>, Self::Error>;
958
959 async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
961
962 async fn revoke_send(
964 &self,
965 operation_id: Self::OperationId,
966 ) -> Result<Self::Amount, Self::Error>;
967
968 async fn check_send_status(&self, operation_id: Self::OperationId)
970 -> Result<bool, Self::Error>;
971
972 async fn mint(
974 &self,
975 quote_id: &str,
976 split_target: SplitTarget,
977 spending_conditions: Option<SpendingConditions>,
978 ) -> Result<Proofs, Self::Error>;
979
980 async fn check_mint_quote_status(&self, quote_id: &str)
982 -> Result<Self::MintQuote, Self::Error>;
983
984 async fn fetch_mint_quote(
986 &self,
987 quote_id: &str,
988 payment_method: Option<Self::PaymentMethod>,
989 ) -> Result<Self::MintQuote, Self::Error>;
990
991 async fn prepare_melt(
993 &self,
994 quote_id: &str,
995 metadata: HashMap<String, String>,
996 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
997
998 async fn prepare_melt_proofs(
1000 &self,
1001 quote_id: &str,
1002 proofs: Proofs,
1003 metadata: HashMap<String, String>,
1004 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1005
1006 async fn prepare_melt_token(
1012 &self,
1013 quote_id: &str,
1014 encoded_token: &str,
1015 metadata: HashMap<String, String>,
1016 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1017
1018 async fn swap(
1020 &self,
1021 amount: Option<Self::Amount>,
1022 split_target: SplitTarget,
1023 input_proofs: Proofs,
1024 spending_conditions: Option<SpendingConditions>,
1025 include_fees: bool,
1026 use_p2bk: bool,
1027 ) -> Result<Option<Proofs>, Self::Error>;
1028
1029 async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
1031
1032 async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
1034
1035 async fn refresh_access_token(&self) -> Result<(), Self::Error>;
1037
1038 async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
1040
1041 async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
1043
1044 async fn restore(&self) -> Result<Restored, Self::Error>;
1046
1047 async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, Self::Error>;
1049
1050 async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
1052
1053 async fn pay_request(
1055 &self,
1056 request: PaymentRequest,
1057 custom_amount: Option<Self::Amount>,
1058 ) -> Result<(), Self::Error>;
1059
1060 async fn subscribe_mint_quote_state(
1065 &self,
1066 quote_ids: Vec<String>,
1067 method: Self::PaymentMethod,
1068 ) -> Result<Self::Subscription, Self::Error>;
1069
1070 fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
1076
1077 async fn subscribe(
1079 &self,
1080 params: Self::SubscribeParams,
1081 ) -> Result<Self::Subscription, Self::Error>;
1082
1083 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1085 async fn melt_bip353_quote(
1086 &self,
1087 bip353_address: &str,
1088 amount_msat: Self::Amount,
1089 network: bitcoin::Network,
1090 ) -> Result<Self::MeltQuote, Self::Error>;
1091
1092 #[cfg(not(target_arch = "wasm32"))]
1094 async fn melt_lightning_address_quote(
1095 &self,
1096 lightning_address: &str,
1097 amount_msat: Self::Amount,
1098 ) -> Result<Self::MeltQuote, Self::Error>;
1099
1100 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1106 async fn melt_human_readable_quote(
1107 &self,
1108 address: &str,
1109 amount_msat: Self::Amount,
1110 network: bitcoin::Network,
1111 ) -> Result<Self::MeltQuote, Self::Error>;
1112
1113 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1115 async fn melt_human_readable(
1116 &self,
1117 address: &str,
1118 amount_msat: Self::Amount,
1119 network: bitcoin::Network,
1120 ) -> Result<Self::MeltQuote, Self::Error> {
1121 self.melt_human_readable_quote(address, amount_msat, network)
1122 .await
1123 }
1124
1125 async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1127 self.check_mint_quote_status(quote_id).await
1128 }
1129
1130 async fn mint_unified(
1132 &self,
1133 quote_id: &str,
1134 split_target: SplitTarget,
1135 spending_conditions: Option<SpendingConditions>,
1136 ) -> Result<Proofs, Self::Error> {
1137 self.mint(quote_id, split_target, spending_conditions).await
1138 }
1139
1140 async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1146
1147 async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1150
1151 async fn get_public_key(
1153 &self,
1154 pubkey: &PublicKey,
1155 ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1156
1157 async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1159
1160 async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1162
1163 async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1165}
1166
1167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1169pub struct P2PKSigningKey {
1170 pub pubkey: PublicKey,
1172 pub derivation_path: DerivationPath,
1174 pub derivation_index: u32,
1176 pub created_time: u64,
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183 use crate::nuts::Id;
1184 use crate::secret::Secret;
1185
1186 #[test]
1187 fn test_transaction_id_from_hex() {
1188 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1189 let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1190 assert_eq!(transaction_id.to_string(), hex_str);
1191 }
1192
1193 #[test]
1194 fn test_transaction_id_from_hex_empty_string() {
1195 let hex_str = "";
1196 let res = TransactionId::from_hex(hex_str);
1197 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1198 }
1199
1200 #[test]
1201 fn test_transaction_id_from_hex_longer_string() {
1202 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1203 let res = TransactionId::from_hex(hex_str);
1204 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1205 }
1206
1207 #[test]
1208 fn test_matches_conditions() {
1209 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1210 let proof = Proof::new(
1211 Amount::from(64),
1212 keyset_id,
1213 Secret::new("test_secret"),
1214 PublicKey::from_hex(
1215 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1216 )
1217 .unwrap(),
1218 );
1219
1220 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1221 let proof_info =
1222 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1223
1224 assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1226 assert!(!proof_info.matches_conditions(
1227 &Some(MintUrl::from_str("https://different.com").unwrap()),
1228 &None,
1229 &None,
1230 &None
1231 ));
1232
1233 assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1235 assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1236
1237 assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1239 assert!(proof_info.matches_conditions(
1240 &None,
1241 &None,
1242 &Some(vec![State::Unspent, State::Spent]),
1243 &None
1244 ));
1245 assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1246
1247 assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1249
1250 assert!(proof_info.matches_conditions(
1252 &Some(mint_url),
1253 &Some(CurrencyUnit::Sat),
1254 &Some(vec![State::Unspent]),
1255 &None
1256 ));
1257 }
1258
1259 #[test]
1260 fn test_matches_conditions_with_spending_conditions() {
1261 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1266 let proof = Proof::new(
1267 Amount::from(64),
1268 keyset_id,
1269 Secret::new("test_secret"),
1270 PublicKey::from_hex(
1271 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1272 )
1273 .unwrap(),
1274 );
1275
1276 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1277 let proof_info =
1278 ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1279
1280 assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1282
1283 let dummy_condition = SpendingConditions::P2PKConditions {
1285 data: SecretKey::generate().public_key(),
1286 conditions: None,
1287 };
1288 assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1289 }
1290
1291 #[test]
1292 fn test_wallet_options_debug_redacts_p2pk_signing_keys() {
1293 let secret_key = SecretKey::generate();
1294 let secret_hex = secret_key.to_secret_hex();
1295
1296 let send_options = SendOptions {
1297 p2pk_signing_keys: vec![secret_key.clone()],
1298 ..Default::default()
1299 };
1300 let receive_options = ReceiveOptions {
1301 p2pk_signing_keys: vec![secret_key],
1302 ..Default::default()
1303 };
1304
1305 let send_debug = format!("{:?}", send_options);
1306 let receive_debug = format!("{:?}", receive_options);
1307
1308 assert!(!send_debug.contains(&secret_hex));
1309 assert!(send_debug.contains("[redacted]"));
1310 assert!(!receive_debug.contains(&secret_hex));
1311 assert!(receive_debug.contains("[redacted]"));
1312 }
1313
1314 #[test]
1315 fn nut13_options_defaults_match_nut13_spec() {
1316 let opts = NUT13Options::default();
1319 assert_eq!(opts.batch_size, NUT13Options::DEFAULT_BATCH_SIZE);
1320 assert_eq!(opts.max_gap, NUT13Options::DEFAULT_MAX_GAP);
1321 }
1322
1323 #[test]
1324 fn nut13_options_new_accepts_custom_values() {
1325 let opts = NUT13Options::new(25, 2).unwrap();
1326 let cloned = opts.clone();
1327 assert_eq!(cloned.batch_size, 25);
1328 assert_eq!(cloned.max_gap, 2);
1329 }
1330
1331 #[test]
1332 fn nut13_options_reject_zero_batch_size() {
1333 let err = NUT13Options::new(0, 2).unwrap_err();
1334 assert!(matches!(
1335 err,
1336 Error::InvalidNut13Options {
1337 field: "batch_size",
1338 ..
1339 }
1340 ));
1341 }
1342
1343 #[test]
1344 fn nut13_options_reject_zero_max_gap() {
1345 let err = NUT13Options::new(25, 0).unwrap_err();
1346 assert!(matches!(
1347 err,
1348 Error::InvalidNut13Options {
1349 field: "max_gap",
1350 ..
1351 }
1352 ));
1353 }
1354}