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;
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};
24#[cfg(feature = "http")]
25use crate::rate_limit::RateLimitConfig;
26use crate::{Amount, Error};
27
28pub mod saga;
29
30pub use saga::{
31 IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
32 ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
33 SwapSagaState, WalletSaga, WalletSagaState,
34};
35
36#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
38pub struct WalletKey {
39 pub mint_url: MintUrl,
41 pub unit: CurrencyUnit,
43}
44
45impl fmt::Display for WalletKey {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
48 }
49}
50
51impl WalletKey {
52 pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
54 Self { mint_url, unit }
55 }
56}
57
58#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ProofInfo {
61 pub proof: Proof,
63 pub y: PublicKey,
65 pub mint_url: MintUrl,
67 pub state: State,
69 pub spending_condition: Option<SpendingConditions>,
71 pub unit: CurrencyUnit,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub used_by_operation: Option<Uuid>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub created_by_operation: Option<Uuid>,
79}
80
81impl ProofInfo {
82 pub fn new(
84 proof: Proof,
85 mint_url: MintUrl,
86 state: State,
87 unit: CurrencyUnit,
88 ) -> Result<Self, Error> {
89 let y = proof.y()?;
90
91 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
92
93 Ok(Self {
94 proof,
95 y,
96 mint_url,
97 state,
98 spending_condition,
99 unit,
100 used_by_operation: None,
101 created_by_operation: None,
102 })
103 }
104
105 pub fn new_with_operations(
107 proof: Proof,
108 mint_url: MintUrl,
109 state: State,
110 unit: CurrencyUnit,
111 used_by_operation: Option<Uuid>,
112 created_by_operation: Option<Uuid>,
113 ) -> Result<Self, Error> {
114 let y = proof.y()?;
115
116 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
117
118 Ok(Self {
119 proof,
120 y,
121 mint_url,
122 state,
123 spending_condition,
124 unit,
125 used_by_operation,
126 created_by_operation,
127 })
128 }
129
130 pub fn matches_conditions(
132 &self,
133 mint_url: &Option<MintUrl>,
134 unit: &Option<CurrencyUnit>,
135 state: &Option<Vec<State>>,
136 spending_conditions: &Option<Vec<SpendingConditions>>,
137 ) -> bool {
138 if let Some(mint_url) = mint_url {
139 if mint_url.ne(&self.mint_url) {
140 return false;
141 }
142 }
143
144 if let Some(unit) = unit {
145 if unit.ne(&self.unit) {
146 return false;
147 }
148 }
149
150 if let Some(state) = state {
151 if !state.contains(&self.state) {
152 return false;
153 }
154 }
155
156 if let Some(spending_conditions) = spending_conditions {
157 match &self.spending_condition {
158 None => {
159 if !spending_conditions.is_empty() {
160 return false;
161 }
162 }
163 Some(s) => {
164 if !spending_conditions.contains(s) {
165 return false;
166 }
167 }
168 }
169 }
170
171 true
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct MintQuote {
178 pub id: String,
180 pub mint_url: MintUrl,
182 pub payment_method: PaymentMethod,
184 pub amount: Option<Amount>,
189 pub unit: CurrencyUnit,
191 pub request: String,
193 pub state: MintQuoteState,
195 pub expiry: u64,
197 pub secret_key: Option<SecretKey>,
199 #[serde(default)]
201 pub amount_issued: Amount,
202 #[serde(default)]
204 pub amount_paid: Amount,
205 #[serde(default)]
207 pub updated_at: u64,
208 pub estimated_blocks: Option<u32>,
210 #[serde(default)]
212 pub used_by_operation: Option<String>,
213 #[serde(default)]
215 pub version: u32,
216}
217
218#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
220pub struct MeltQuote {
221 pub id: String,
223 pub mint_url: Option<MintUrl>,
225 pub unit: CurrencyUnit,
227 pub amount: Amount,
229 pub request: String,
231 pub fee_reserve: Amount,
233 pub state: MeltQuoteState,
235 pub expiry: u64,
237 #[serde(alias = "payment_preimage")]
239 pub payment_proof: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub estimated_blocks: Option<u32>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub fee_index: Option<u32>,
246 pub payment_method: PaymentMethod,
248 #[serde(default)]
250 pub used_by_operation: Option<String>,
251 #[serde(default)]
253 pub version: u32,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct CrossMintTransferQuote {
259 pub mint_quote: MintQuote,
261 pub melt_quote: MeltQuote,
263 pub input_fee: Amount,
265}
266
267impl MintQuote {
268 #[allow(clippy::too_many_arguments)]
270 pub fn new(
271 id: String,
272 mint_url: MintUrl,
273 payment_method: PaymentMethod,
274 amount: Option<Amount>,
275 unit: CurrencyUnit,
276 request: String,
277 expiry: u64,
278 secret_key: Option<SecretKey>,
279 ) -> Self {
280 Self {
281 id,
282 mint_url,
283 payment_method,
284 amount,
285 unit,
286 request,
287 state: MintQuoteState::Unpaid,
288 expiry,
289 secret_key,
290 amount_issued: Amount::ZERO,
291 amount_paid: Amount::ZERO,
292 updated_at: 0,
293 estimated_blocks: None,
294 used_by_operation: None,
295 version: 0,
296 }
297 }
298
299 pub fn total_amount(&self) -> Amount {
301 self.amount_paid
302 }
303
304 pub fn state_from_amounts(&self) -> MintQuoteState {
306 quote_state_from_amounts(self.amount_paid, self.amount_issued).unwrap_or(self.state)
307 }
308
309 pub fn update_state_from_amounts(&mut self) {
311 self.state = self.state_from_amounts();
312 }
313
314 pub fn is_expired(&self, current_time: u64) -> bool {
316 current_time > self.expiry
317 }
318
319 pub fn amount_mintable(&self) -> Amount {
321 if self.payment_method == PaymentMethod::BOLT11 {
322 if self.state == MintQuoteState::Paid {
324 self.amount.unwrap_or(Amount::ZERO)
325 } else {
326 Amount::ZERO
327 }
328 } else {
329 self.amount_paid
331 .checked_sub(self.amount_issued)
332 .unwrap_or(Amount::ZERO)
333 }
334 }
335}
336
337#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
339pub struct Restored {
340 pub spent: Amount,
342 pub unspent: Amount,
344 pub pending: Amount,
346}
347
348#[derive(Debug, Clone)]
356pub struct NUT13Options {
357 pub batch_size: u32,
359 pub max_gap: u32,
361}
362
363impl Default for NUT13Options {
364 fn default() -> Self {
365 Self {
366 batch_size: Self::DEFAULT_BATCH_SIZE,
367 max_gap: Self::DEFAULT_MAX_GAP,
368 }
369 }
370}
371
372impl NUT13Options {
373 pub const DEFAULT_BATCH_SIZE: u32 = 100;
375
376 pub const DEFAULT_MAX_GAP: u32 = 3;
378
379 pub fn new(batch_size: u32, max_gap: u32) -> Result<Self, Error> {
381 let opts = Self {
382 batch_size,
383 max_gap,
384 };
385 opts.validate()?;
386 Ok(opts)
387 }
388
389 pub(crate) fn validate(&self) -> Result<(), Error> {
390 if self.batch_size == 0 {
391 return Err(Error::InvalidNut13Options {
392 field: "batch_size",
393 reason: "must be greater than zero",
394 });
395 }
396
397 if self.max_gap == 0 {
398 return Err(Error::InvalidNut13Options {
399 field: "max_gap",
400 reason: "must be greater than zero",
401 });
402 }
403
404 Ok(())
405 }
406}
407
408#[derive(Clone, Default)]
410pub struct SendOptions {
411 pub memo: Option<SendMemo>,
413 pub conditions: Option<SpendingConditions>,
415 pub amount_split_target: SplitTarget,
417 pub send_kind: SendKind,
419 pub include_fee: bool,
421 pub max_proofs: Option<usize>,
423 pub metadata: HashMap<String, String>,
425 pub use_p2bk: bool,
427 pub p2pk_signing_keys: Vec<SecretKey>,
429 pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
431}
432
433impl fmt::Debug for SendOptions {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 f.debug_struct("SendOptions")
436 .field("memo", &self.memo)
437 .field("conditions", &self.conditions)
438 .field("amount_split_target", &self.amount_split_target)
439 .field("send_kind", &self.send_kind)
440 .field("include_fee", &self.include_fee)
441 .field("max_proofs", &self.max_proofs)
442 .field("metadata", &self.metadata)
443 .field("use_p2bk", &self.use_p2bk)
444 .field("p2pk_signing_keys", &"[redacted]")
445 .field(
446 "p2pk_locked_proof_send_mode",
447 &self.p2pk_locked_proof_send_mode,
448 )
449 .finish()
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
455pub enum P2PKLockedProofSendMode {
456 #[default]
458 Swap,
459 SignAndSend,
461}
462
463#[derive(Debug, Clone)]
465pub struct SendMemo {
466 pub memo: String,
468 pub include_memo: bool,
470}
471
472impl SendMemo {
473 pub fn for_token(memo: &str) -> Self {
475 Self {
476 memo: memo.to_string(),
477 include_memo: true,
478 }
479 }
480}
481
482#[derive(Clone, Default)]
484pub struct ReceiveOptions {
485 pub amount_split_target: SplitTarget,
487 pub p2pk_signing_keys: Vec<SecretKey>,
489 pub preimages: Vec<String>,
491 pub metadata: HashMap<String, String>,
493}
494
495impl fmt::Debug for ReceiveOptions {
496 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 f.debug_struct("ReceiveOptions")
498 .field("amount_split_target", &self.amount_split_target)
499 .field("p2pk_signing_keys", &"[redacted]")
500 .field("preimages", &"[redacted]")
501 .field("metadata", &self.metadata)
502 .finish()
503 }
504}
505
506#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
508pub enum SendKind {
509 #[default]
510 OnlineExact,
512 OnlineTolerance(Amount),
514 OfflineExact,
516 OfflineTolerance(Amount),
518}
519
520impl SendKind {
521 pub fn is_online(&self) -> bool {
523 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
524 }
525
526 pub fn is_offline(&self) -> bool {
528 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
529 }
530
531 pub fn is_exact(&self) -> bool {
533 matches!(self, Self::OnlineExact | Self::OfflineExact)
534 }
535
536 pub fn has_tolerance(&self) -> bool {
538 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
539 }
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
544pub struct Transaction {
545 pub mint_url: MintUrl,
547 pub direction: TransactionDirection,
549 pub amount: Amount,
551 pub fee: Amount,
553 pub unit: CurrencyUnit,
555 pub ys: Vec<PublicKey>,
557 pub timestamp: u64,
559 pub memo: Option<String>,
561 pub metadata: HashMap<String, String>,
563 pub quote_id: Option<String>,
565 pub payment_request: Option<String>,
567 #[serde(alias = "payment_preimage")]
569 pub payment_proof: Option<String>,
570 #[serde(default)]
572 pub payment_method: Option<PaymentMethod>,
573 #[serde(default)]
575 pub saga_id: Option<Uuid>,
576 #[serde(default)]
578 pub status: TransactionStatus,
579}
580
581impl Transaction {
582 pub fn id(&self) -> TransactionId {
588 match self.saga_id {
589 Some(saga_id) => TransactionId::from_saga_id(saga_id),
590 None => TransactionId::new(self.ys.clone()),
591 }
592 }
593
594 pub fn matches_conditions(
596 &self,
597 mint_url: &Option<MintUrl>,
598 direction: &Option<TransactionDirection>,
599 unit: &Option<CurrencyUnit>,
600 ) -> bool {
601 if let Some(mint_url) = mint_url {
602 if &self.mint_url != mint_url {
603 return false;
604 }
605 }
606 if let Some(direction) = direction {
607 if &self.direction != direction {
608 return false;
609 }
610 }
611 if let Some(unit) = unit {
612 if &self.unit != unit {
613 return false;
614 }
615 }
616 true
617 }
618}
619
620impl PartialOrd for Transaction {
621 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
622 Some(self.cmp(other))
623 }
624}
625
626impl Ord for Transaction {
627 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
628 self.timestamp
629 .cmp(&other.timestamp)
630 .reverse()
631 .then_with(|| self.id().cmp(&other.id()))
632 }
633}
634
635#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
637pub enum TransactionDirection {
638 Incoming,
640 Outgoing,
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
646#[serde(rename_all = "snake_case")]
647pub enum TransactionStatus {
648 Pending,
650 #[default]
652 Completed,
653 Failed,
655}
656
657impl fmt::Display for TransactionStatus {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 match self {
660 Self::Pending => write!(f, "pending"),
661 Self::Completed => write!(f, "completed"),
662 Self::Failed => write!(f, "failed"),
663 }
664 }
665}
666
667impl FromStr for TransactionStatus {
668 type Err = Error;
669
670 fn from_str(value: &str) -> Result<Self, Self::Err> {
671 match value {
672 "pending" => Ok(Self::Pending),
673 "completed" => Ok(Self::Completed),
674 "failed" => Ok(Self::Failed),
675 _ => Err(Error::InvalidTransactionStatus),
676 }
677 }
678}
679
680impl std::fmt::Display for TransactionDirection {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 match self {
683 TransactionDirection::Incoming => write!(f, "Incoming"),
684 TransactionDirection::Outgoing => write!(f, "Outgoing"),
685 }
686 }
687}
688
689impl FromStr for TransactionDirection {
690 type Err = Error;
691
692 fn from_str(value: &str) -> Result<Self, Self::Err> {
693 match value {
694 "Incoming" => Ok(Self::Incoming),
695 "Outgoing" => Ok(Self::Outgoing),
696 _ => Err(Error::InvalidTransactionDirection),
697 }
698 }
699}
700
701#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
703#[serde(transparent)]
704pub struct TransactionId([u8; 32]);
705
706impl TransactionId {
707 pub fn new(ys: Vec<PublicKey>) -> Self {
711 let mut ys = ys;
712 ys.sort();
713 let mut hasher = sha256::Hash::engine();
714 for y in ys {
715 hasher.input(&y.to_bytes());
716 }
717 let hash = sha256::Hash::from_engine(hasher);
718 Self(hash.to_byte_array())
719 }
720
721 pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
725 let ys = proofs
726 .iter()
727 .map(|proof| proof.y())
728 .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
729 Ok(Self::new(ys))
730 }
731
732 pub fn from_saga_id(saga_id: Uuid) -> Self {
737 let mut bytes = [0_u8; 32];
738 let encoded = saga_id.simple().to_string();
739 for (destination, source) in bytes.iter_mut().zip(encoded.bytes()) {
740 *destination = source;
741 }
742 Self(bytes)
743 }
744
745 pub fn from_bytes(bytes: [u8; 32]) -> Self {
747 Self(bytes)
748 }
749
750 pub fn from_hex(value: &str) -> Result<Self, Error> {
752 let bytes = hex::decode(value)?;
753 if bytes.len() != 32 {
754 return Err(Error::InvalidTransactionId);
755 }
756 let mut array = [0u8; 32];
757 array.copy_from_slice(&bytes);
758 Ok(Self(array))
759 }
760
761 pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
763 if slice.len() != 32 {
764 return Err(Error::InvalidTransactionId);
765 }
766 let mut array = [0u8; 32];
767 array.copy_from_slice(slice);
768 Ok(Self(array))
769 }
770
771 pub fn as_bytes(&self) -> &[u8; 32] {
773 &self.0
774 }
775
776 pub fn as_slice(&self) -> &[u8] {
778 &self.0
779 }
780}
781
782impl std::fmt::Display for TransactionId {
783 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784 write!(f, "{}", hex::encode(self.0))
785 }
786}
787
788impl FromStr for TransactionId {
789 type Err = Error;
790
791 fn from_str(value: &str) -> Result<Self, Self::Err> {
792 Self::from_hex(value)
793 }
794}
795
796impl TryFrom<Proofs> for TransactionId {
797 type Error = nut00::Error;
798
799 fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
800 Self::from_proofs(proofs)
801 }
802}
803
804#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
806#[serde(rename_all = "snake_case")]
807pub enum OperationKind {
808 Send,
810 Receive,
812 Swap,
814 Mint,
816 Melt,
818}
819
820impl fmt::Display for OperationKind {
821 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
822 match self {
823 OperationKind::Send => write!(f, "send"),
824 OperationKind::Receive => write!(f, "receive"),
825 OperationKind::Swap => write!(f, "swap"),
826 OperationKind::Mint => write!(f, "mint"),
827 OperationKind::Melt => write!(f, "melt"),
828 }
829 }
830}
831
832impl FromStr for OperationKind {
833 type Err = Error;
834
835 fn from_str(s: &str) -> Result<Self, Self::Err> {
836 match s {
837 "send" => Ok(OperationKind::Send),
838 "receive" => Ok(OperationKind::Receive),
839 "swap" => Ok(OperationKind::Swap),
840 "mint" => Ok(OperationKind::Mint),
841 "melt" => Ok(OperationKind::Melt),
842 _ => Err(Error::InvalidOperationKind),
843 }
844 }
845}
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub enum KeysetFilter {
850 Active,
852 All,
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
861pub enum KeysetLoadPolicy {
862 CacheOnly,
865 #[default]
871 CacheThenNetwork,
872 Refresh,
876}
877
878#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
887#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
888pub trait Wallet: Send + Sync {
889 type Error: std::error::Error + Send + Sync + 'static;
891 type Amount: Clone + Send + Sync;
893 type MintUrl: Clone + Send + Sync;
895 type CurrencyUnit: Clone + Send + Sync;
897 type MintInfo: Clone + Send + Sync;
899 type KeySetInfo: Clone + Send + Sync;
901 type MintQuote: Clone + Send + Sync;
903 type MeltQuote: Clone + Send + Sync;
905 type CrossMintTransferQuote: Clone + Send + Sync;
907 type PaymentMethod: Clone + Send + Sync;
909 type MeltOptions: Clone + Send + Sync;
911 type OperationId: Clone + Send + Sync;
913 type PreparedSend<'a>: Send + Sync
915 where
916 Self: 'a;
917 type PreparedMelt<'a>: Send + Sync
919 where
920 Self: 'a;
921 type Subscription: Send + Sync;
923 type SubscribeParams: Clone + Send + Sync;
925 type RecoveryReport: Clone + Send + Sync;
927
928 fn mint_url(&self) -> Self::MintUrl;
930
931 fn unit(&self) -> Self::CurrencyUnit;
933
934 async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
936
937 async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
939
940 async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
942
943 async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
945
946 async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
948
949 async fn keysets(&self, policy: KeysetLoadPolicy)
957 -> Result<Vec<Self::KeySetInfo>, Self::Error>;
958
959 async fn active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
964
965 async fn keyset(&self, keyset_id: Id) -> Result<Self::KeySetInfo, Self::Error>;
967
968 async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
970
971 async fn get_keyset_count_fee(
973 &self,
974 keyset_id: &Id,
975 count: u64,
976 ) -> Result<Self::Amount, Self::Error>;
977
978 async fn get_keyset_fees_and_amounts_by_id(
980 &self,
981 keyset_id: Id,
982 ) -> Result<FeeAndAmounts, Self::Error>;
983
984 async fn mint_quote(
986 &self,
987 method: Self::PaymentMethod,
988 amount: Option<Self::Amount>,
989 description: Option<String>,
990 extra: Option<String>,
991 ) -> Result<Self::MintQuote, Self::Error>;
992
993 async fn melt_quote(
995 &self,
996 method: Self::PaymentMethod,
997 request: String,
998 options: Option<Self::MeltOptions>,
999 extra: Option<String>,
1000 ) -> Result<Self::MeltQuote, Self::Error>;
1001
1002 async fn cross_mint_transfer_quote_max(
1011 &self,
1012 target_wallet: &Self,
1013 ) -> Result<Self::CrossMintTransferQuote, Self::Error>;
1014
1015 async fn list_transactions(
1017 &self,
1018 direction: Option<TransactionDirection>,
1019 ) -> Result<Vec<Transaction>, Self::Error>;
1020
1021 async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
1023
1024 async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
1026
1027 async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
1029
1030 async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
1032
1033 async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
1035
1036 async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
1038
1039 async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
1041
1042 async fn calculate_fee(
1044 &self,
1045 proof_count: u64,
1046 keyset_id: Id,
1047 ) -> Result<Self::Amount, Self::Error>;
1048
1049 async fn receive(
1051 &self,
1052 encoded_token: &str,
1053 options: ReceiveOptions,
1054 ) -> Result<Self::Amount, Self::Error>;
1055
1056 async fn receive_proofs(
1058 &self,
1059 proofs: Proofs,
1060 options: ReceiveOptions,
1061 memo: Option<String>,
1062 token: Option<String>,
1063 ) -> Result<Self::Amount, Self::Error>;
1064
1065 async fn prepare_send(
1067 &self,
1068 amount: Self::Amount,
1069 options: SendOptions,
1070 ) -> Result<Self::PreparedSend<'_>, Self::Error>;
1071
1072 async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
1074
1075 async fn revoke_send(
1077 &self,
1078 operation_id: Self::OperationId,
1079 ) -> Result<Self::Amount, Self::Error>;
1080
1081 async fn check_send_status(&self, operation_id: Self::OperationId)
1083 -> Result<bool, Self::Error>;
1084
1085 async fn mint(
1087 &self,
1088 quote_id: &str,
1089 split_target: SplitTarget,
1090 spending_conditions: Option<SpendingConditions>,
1091 ) -> Result<Proofs, Self::Error>;
1092
1093 async fn mint_unissued_quotes(&self) -> Result<Self::Amount, Self::Error>;
1095
1096 async fn check_mint_quote_status(&self, quote_id: &str)
1098 -> Result<Self::MintQuote, Self::Error>;
1099
1100 async fn fetch_mint_quote(
1102 &self,
1103 quote_id: &str,
1104 payment_method: Option<Self::PaymentMethod>,
1105 ) -> Result<Self::MintQuote, Self::Error>;
1106
1107 async fn prepare_melt(
1109 &self,
1110 quote_id: &str,
1111 metadata: HashMap<String, String>,
1112 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1113
1114 async fn prepare_melt_proofs(
1116 &self,
1117 quote_id: &str,
1118 proofs: Proofs,
1119 metadata: HashMap<String, String>,
1120 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1121
1122 async fn prepare_melt_token(
1128 &self,
1129 quote_id: &str,
1130 encoded_token: &str,
1131 metadata: HashMap<String, String>,
1132 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1133
1134 async fn swap(
1136 &self,
1137 amount: Option<Self::Amount>,
1138 split_target: SplitTarget,
1139 input_proofs: Proofs,
1140 spending_conditions: Option<SpendingConditions>,
1141 include_fees: bool,
1142 use_p2bk: bool,
1143 ) -> Result<Option<Proofs>, Self::Error>;
1144
1145 async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
1147
1148 async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
1150
1151 async fn refresh_access_token(&self) -> Result<(), Self::Error>;
1153
1154 async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
1156
1157 async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
1159
1160 async fn restore(&self) -> Result<Restored, Self::Error>;
1162
1163 async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, Self::Error>;
1165
1166 async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
1168
1169 async fn pay_request(
1171 &self,
1172 request: PaymentRequest,
1173 custom_amount: Option<Self::Amount>,
1174 ) -> Result<(), Self::Error>;
1175
1176 async fn subscribe_mint_quote_state(
1181 &self,
1182 quote_ids: Vec<String>,
1183 method: Self::PaymentMethod,
1184 ) -> Result<Self::Subscription, Self::Error>;
1185
1186 fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
1192
1193 #[cfg(feature = "http")]
1199 fn set_rate_limiting_config(&self, config: Option<RateLimitConfig>);
1200
1201 #[cfg(feature = "http")]
1206 fn is_rate_limited(&self) -> bool;
1207
1208 #[cfg(feature = "http")]
1214 async fn flush_rate_limits(&self);
1215
1216 async fn subscribe(
1218 &self,
1219 params: Self::SubscribeParams,
1220 ) -> Result<Self::Subscription, Self::Error>;
1221
1222 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1224 async fn melt_bip353_quote(
1225 &self,
1226 bip353_address: &str,
1227 amount_msat: Self::Amount,
1228 network: bitcoin::Network,
1229 ) -> Result<Self::MeltQuote, Self::Error>;
1230
1231 #[cfg(not(target_arch = "wasm32"))]
1233 async fn melt_lightning_address_quote(
1234 &self,
1235 lightning_address: &str,
1236 amount_msat: Self::Amount,
1237 ) -> Result<Self::MeltQuote, Self::Error>;
1238
1239 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1245 async fn melt_human_readable_quote(
1246 &self,
1247 address: &str,
1248 amount_msat: Self::Amount,
1249 network: bitcoin::Network,
1250 ) -> Result<Self::MeltQuote, Self::Error>;
1251
1252 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1254 async fn melt_human_readable(
1255 &self,
1256 address: &str,
1257 amount_msat: Self::Amount,
1258 network: bitcoin::Network,
1259 ) -> Result<Self::MeltQuote, Self::Error> {
1260 self.melt_human_readable_quote(address, amount_msat, network)
1261 .await
1262 }
1263
1264 async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1266 self.check_mint_quote_status(quote_id).await
1267 }
1268
1269 async fn mint_unified(
1271 &self,
1272 quote_id: &str,
1273 split_target: SplitTarget,
1274 spending_conditions: Option<SpendingConditions>,
1275 ) -> Result<Proofs, Self::Error> {
1276 self.mint(quote_id, split_target, spending_conditions).await
1277 }
1278
1279 async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1285
1286 async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1289
1290 async fn get_public_key(
1292 &self,
1293 pubkey: &PublicKey,
1294 ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1295
1296 async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1298
1299 async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1301
1302 async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1304}
1305
1306#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1308pub struct P2PKSigningKey {
1309 pub pubkey: PublicKey,
1311 pub derivation_path: DerivationPath,
1313 pub derivation_index: u32,
1315 pub created_time: u64,
1317}
1318
1319#[cfg(test)]
1320mod tests {
1321 use super::*;
1322 use crate::nuts::Id;
1323 use crate::secret::Secret;
1324
1325 #[test]
1326 fn test_transaction_id_from_hex() {
1327 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1328 let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1329 assert_eq!(transaction_id.to_string(), hex_str);
1330 }
1331
1332 #[test]
1333 fn test_transaction_id_from_hex_empty_string() {
1334 let hex_str = "";
1335 let res = TransactionId::from_hex(hex_str);
1336 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1337 }
1338
1339 #[test]
1340 fn test_transaction_id_from_hex_longer_string() {
1341 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1342 let res = TransactionId::from_hex(hex_str);
1343 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1344 }
1345
1346 #[test]
1347 fn transaction_id_from_saga_id_uses_canonical_uuid_bytes() {
1348 let saga_id =
1349 Uuid::parse_str("019fa338-b72f-7f21-9bb2-a504cdd5927b").expect("valid saga ID");
1350 let transaction_id = TransactionId::from_saga_id(saga_id);
1351
1352 assert_eq!(
1353 transaction_id.as_bytes(),
1354 b"019fa338b72f7f219bb2a504cdd5927b"
1355 );
1356 }
1357
1358 #[test]
1359 fn saga_managed_transactions_with_the_same_ys_have_distinct_ids() {
1360 let ys = vec![SecretKey::generate().public_key()];
1361 let transaction = Transaction {
1362 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1363 direction: TransactionDirection::Outgoing,
1364 amount: Amount::from(10),
1365 fee: Amount::ZERO,
1366 unit: CurrencyUnit::Sat,
1367 ys,
1368 timestamp: 42,
1369 memo: None,
1370 metadata: HashMap::new(),
1371 quote_id: None,
1372 payment_request: None,
1373 payment_proof: None,
1374 payment_method: None,
1375 saga_id: Some(Uuid::new_v4()),
1376 status: TransactionStatus::Pending,
1377 };
1378 let mut received = transaction.clone();
1379 received.direction = TransactionDirection::Incoming;
1380 received.saga_id = Some(Uuid::new_v4());
1381
1382 assert_ne!(transaction.id(), received.id());
1383 }
1384
1385 #[test]
1386 fn test_matches_conditions() {
1387 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1388 let proof = Proof::new(
1389 Amount::from(64),
1390 keyset_id,
1391 Secret::new("test_secret"),
1392 PublicKey::from_hex(
1393 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1394 )
1395 .unwrap(),
1396 );
1397
1398 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1399 let proof_info =
1400 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1401
1402 assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1404 assert!(!proof_info.matches_conditions(
1405 &Some(MintUrl::from_str("https://different.com").unwrap()),
1406 &None,
1407 &None,
1408 &None
1409 ));
1410
1411 assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1413 assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1414
1415 assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1417 assert!(proof_info.matches_conditions(
1418 &None,
1419 &None,
1420 &Some(vec![State::Unspent, State::Spent]),
1421 &None
1422 ));
1423 assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1424
1425 assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1427
1428 assert!(proof_info.matches_conditions(
1430 &Some(mint_url),
1431 &Some(CurrencyUnit::Sat),
1432 &Some(vec![State::Unspent]),
1433 &None
1434 ));
1435 }
1436
1437 #[test]
1438 fn test_matches_conditions_with_spending_conditions() {
1439 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1444 let proof = Proof::new(
1445 Amount::from(64),
1446 keyset_id,
1447 Secret::new("test_secret"),
1448 PublicKey::from_hex(
1449 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1450 )
1451 .unwrap(),
1452 );
1453
1454 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1455 let proof_info =
1456 ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1457
1458 assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1460
1461 let dummy_condition = SpendingConditions::P2PKConditions {
1463 data: SecretKey::generate().public_key(),
1464 conditions: None,
1465 };
1466 assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1467 }
1468
1469 #[test]
1470 fn test_wallet_options_debug_redacts_p2pk_signing_keys() {
1471 let secret_key = SecretKey::generate();
1472 let secret_hex = secret_key.to_secret_hex();
1473 let preimage = "super_secret_htlc_preimage_xyz";
1474
1475 let send_options = SendOptions {
1476 p2pk_signing_keys: vec![secret_key.clone()],
1477 ..Default::default()
1478 };
1479 let receive_options = ReceiveOptions {
1480 p2pk_signing_keys: vec![secret_key],
1481 preimages: vec![preimage.to_string()],
1482 ..Default::default()
1483 };
1484
1485 let send_debug = format!("{:?}", send_options);
1486 let receive_debug = format!("{:?}", receive_options);
1487
1488 assert!(!send_debug.contains(&secret_hex));
1489 assert!(send_debug.contains("[redacted]"));
1490 assert!(!receive_debug.contains(&secret_hex));
1491 assert!(!receive_debug.contains(preimage));
1492 assert!(receive_debug.contains("[redacted]"));
1493 }
1494
1495 #[test]
1496 fn nut13_options_defaults_match_nut13_spec() {
1497 let opts = NUT13Options::default();
1500 assert_eq!(opts.batch_size, NUT13Options::DEFAULT_BATCH_SIZE);
1501 assert_eq!(opts.max_gap, NUT13Options::DEFAULT_MAX_GAP);
1502 }
1503
1504 #[test]
1505 fn nut13_options_new_accepts_custom_values() {
1506 let opts = NUT13Options::new(25, 2).unwrap();
1507 let cloned = opts.clone();
1508 assert_eq!(cloned.batch_size, 25);
1509 assert_eq!(cloned.max_gap, 2);
1510 }
1511
1512 #[test]
1513 fn nut13_options_reject_zero_batch_size() {
1514 let err = NUT13Options::new(0, 2).unwrap_err();
1515 assert!(matches!(
1516 err,
1517 Error::InvalidNut13Options {
1518 field: "batch_size",
1519 ..
1520 }
1521 ));
1522 }
1523
1524 #[test]
1525 fn nut13_options_reject_zero_max_gap() {
1526 let err = NUT13Options::new(25, 0).unwrap_err();
1527 assert!(matches!(
1528 err,
1529 Error::InvalidNut13Options {
1530 field: "max_gap",
1531 ..
1532 }
1533 ));
1534 }
1535
1536 #[test]
1537 fn transaction_status_round_trips_and_rejects_unknown_values() {
1538 for status in [
1539 TransactionStatus::Pending,
1540 TransactionStatus::Completed,
1541 TransactionStatus::Failed,
1542 ] {
1543 assert_eq!(
1544 TransactionStatus::from_str(&status.to_string()).expect("valid status"),
1545 status
1546 );
1547 }
1548
1549 assert!(matches!(
1550 TransactionStatus::from_str("unknown"),
1551 Err(Error::InvalidTransactionStatus)
1552 ));
1553 }
1554
1555 #[test]
1556 fn transaction_without_status_defaults_to_completed() {
1557 let transaction = Transaction {
1558 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1559 direction: TransactionDirection::Incoming,
1560 amount: Amount::from(10),
1561 fee: Amount::ZERO,
1562 unit: CurrencyUnit::Sat,
1563 ys: vec![SecretKey::generate().public_key()],
1564 timestamp: 42,
1565 memo: None,
1566 metadata: HashMap::new(),
1567 quote_id: None,
1568 payment_request: None,
1569 payment_proof: None,
1570 payment_method: None,
1571 saga_id: None,
1572 status: TransactionStatus::Pending,
1573 };
1574 let mut value = serde_json::to_value(transaction).expect("serialize transaction");
1575 value
1576 .as_object_mut()
1577 .expect("transaction serializes as an object")
1578 .remove("status");
1579
1580 let decoded: Transaction =
1581 serde_json::from_value(value).expect("deserialize legacy transaction");
1582
1583 assert_eq!(decoded.status, TransactionStatus::Completed);
1584 }
1585}