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::AuthProof;
13use cashu::util::hex;
14use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
15use serde::{Deserialize, Serialize};
16use uuid::Uuid;
17
18use crate::mint_quote::quote_state_from_amounts;
19use crate::mint_url::MintUrl;
20use crate::nuts::{
21 CurrencyUnit, Id, MeltQuoteState, MintQuoteState, SecretKey, SpendingConditions, State,
22};
23#[cfg(feature = "http")]
24use crate::rate_limit::RateLimitConfig;
25use crate::{Amount, Error};
26
27pub mod saga;
28
29pub use saga::{
30 IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
31 ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
32 SwapSagaState, WalletSaga, WalletSagaState,
33};
34
35#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37pub struct WalletKey {
38 pub mint_url: MintUrl,
40 pub unit: CurrencyUnit,
42}
43
44impl fmt::Display for WalletKey {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
47 }
48}
49
50impl WalletKey {
51 pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
53 Self { mint_url, unit }
54 }
55}
56
57#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ProofInfo {
60 pub proof: Proof,
62 pub y: PublicKey,
64 pub mint_url: MintUrl,
66 pub state: State,
68 pub spending_condition: Option<SpendingConditions>,
70 pub unit: CurrencyUnit,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub used_by_operation: Option<Uuid>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub created_by_operation: Option<Uuid>,
78}
79
80impl fmt::Debug for ProofInfo {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.debug_struct("ProofInfo")
83 .field("amount", &self.proof.amount)
84 .field("keyset_id", &self.proof.keyset_id)
85 .field("proof", &"[REDACTED]")
86 .field("y", &self.y)
87 .field("mint_url", &self.mint_url)
88 .field("state", &self.state)
89 .field("spending_condition", &self.spending_condition)
90 .field("unit", &self.unit)
91 .field("used_by_operation", &self.used_by_operation)
92 .field("created_by_operation", &self.created_by_operation)
93 .finish()
94 }
95}
96
97impl ProofInfo {
98 pub fn new(
100 proof: Proof,
101 mint_url: MintUrl,
102 state: State,
103 unit: CurrencyUnit,
104 ) -> Result<Self, Error> {
105 let y = proof.y()?;
106
107 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
108
109 Ok(Self {
110 proof,
111 y,
112 mint_url,
113 state,
114 spending_condition,
115 unit,
116 used_by_operation: None,
117 created_by_operation: None,
118 })
119 }
120
121 pub fn new_with_operations(
123 proof: Proof,
124 mint_url: MintUrl,
125 state: State,
126 unit: CurrencyUnit,
127 used_by_operation: Option<Uuid>,
128 created_by_operation: Option<Uuid>,
129 ) -> Result<Self, Error> {
130 let y = proof.y()?;
131
132 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
133
134 Ok(Self {
135 proof,
136 y,
137 mint_url,
138 state,
139 spending_condition,
140 unit,
141 used_by_operation,
142 created_by_operation,
143 })
144 }
145
146 pub fn matches_conditions(
148 &self,
149 mint_url: &Option<MintUrl>,
150 unit: &Option<CurrencyUnit>,
151 state: &Option<Vec<State>>,
152 spending_conditions: &Option<Vec<SpendingConditions>>,
153 ) -> bool {
154 if let Some(mint_url) = mint_url {
155 if mint_url.ne(&self.mint_url) {
156 return false;
157 }
158 }
159
160 if let Some(unit) = unit {
161 if unit.ne(&self.unit) {
162 return false;
163 }
164 }
165
166 if let Some(state) = state {
167 if !state.contains(&self.state) {
168 return false;
169 }
170 }
171
172 if let Some(spending_conditions) = spending_conditions {
173 match &self.spending_condition {
174 None => {
175 if !spending_conditions.is_empty() {
176 return false;
177 }
178 }
179 Some(s) => {
180 if !spending_conditions.contains(s) {
181 return false;
182 }
183 }
184 }
185 }
186
187 true
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct MintQuote {
194 pub id: String,
196 pub mint_url: MintUrl,
198 pub payment_method: PaymentMethod,
200 pub amount: Option<Amount>,
205 pub unit: CurrencyUnit,
207 pub request: String,
209 pub state: MintQuoteState,
211 pub expiry: u64,
213 pub secret_key: Option<SecretKey>,
215 #[serde(default)]
217 pub amount_issued: Amount,
218 #[serde(default)]
220 pub amount_paid: Amount,
221 #[serde(default)]
223 pub updated_at: u64,
224 pub estimated_blocks: Option<u32>,
226 #[serde(default)]
228 pub used_by_operation: Option<String>,
229 #[serde(default)]
231 pub version: u32,
232}
233
234#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
236pub struct MeltQuote {
237 pub id: String,
239 pub mint_url: Option<MintUrl>,
241 pub unit: CurrencyUnit,
243 pub amount: Amount,
245 pub request: String,
247 pub fee_reserve: Amount,
249 pub state: MeltQuoteState,
251 pub expiry: u64,
253 #[serde(alias = "payment_preimage")]
255 pub payment_proof: Option<String>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub estimated_blocks: Option<u32>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub fee_index: Option<u32>,
262 pub payment_method: PaymentMethod,
264 #[serde(default)]
266 pub used_by_operation: Option<String>,
267 #[serde(default)]
269 pub version: u32,
270}
271
272impl fmt::Debug for MeltQuote {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 f.debug_struct("MeltQuote")
275 .field("id", &self.id)
276 .field("mint_url", &self.mint_url)
277 .field("unit", &self.unit)
278 .field("amount", &self.amount)
279 .field("request", &self.request)
280 .field("fee_reserve", &self.fee_reserve)
281 .field("state", &self.state)
282 .field("expiry", &self.expiry)
283 .field(
284 "payment_proof",
285 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
286 )
287 .field("estimated_blocks", &self.estimated_blocks)
288 .field("fee_index", &self.fee_index)
289 .field("payment_method", &self.payment_method)
290 .field("used_by_operation", &self.used_by_operation)
291 .field("version", &self.version)
292 .finish()
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct CrossMintTransferQuote {
299 pub mint_quote: MintQuote,
301 pub melt_quote: MeltQuote,
303 pub input_fee: Amount,
305}
306
307impl MintQuote {
308 #[allow(clippy::too_many_arguments)]
310 pub fn new(
311 id: String,
312 mint_url: MintUrl,
313 payment_method: PaymentMethod,
314 amount: Option<Amount>,
315 unit: CurrencyUnit,
316 request: String,
317 expiry: u64,
318 secret_key: Option<SecretKey>,
319 ) -> Self {
320 Self {
321 id,
322 mint_url,
323 payment_method,
324 amount,
325 unit,
326 request,
327 state: MintQuoteState::Unpaid,
328 expiry,
329 secret_key,
330 amount_issued: Amount::ZERO,
331 amount_paid: Amount::ZERO,
332 updated_at: 0,
333 estimated_blocks: None,
334 used_by_operation: None,
335 version: 0,
336 }
337 }
338
339 pub fn total_amount(&self) -> Amount {
341 self.amount_paid
342 }
343
344 pub fn state_from_amounts(&self) -> MintQuoteState {
346 quote_state_from_amounts(self.amount_paid, self.amount_issued).unwrap_or(self.state)
347 }
348
349 pub fn update_state_from_amounts(&mut self) {
351 self.state = self.state_from_amounts();
352 }
353
354 pub fn is_expired(&self, current_time: u64) -> bool {
356 current_time > self.expiry
357 }
358
359 pub fn amount_mintable(&self) -> Amount {
361 if self.payment_method == PaymentMethod::BOLT11 {
362 if self.state == MintQuoteState::Paid {
364 self.amount.unwrap_or(Amount::ZERO)
365 } else {
366 Amount::ZERO
367 }
368 } else {
369 self.amount_paid
371 .checked_sub(self.amount_issued)
372 .unwrap_or(Amount::ZERO)
373 }
374 }
375}
376
377#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
379pub struct Restored {
380 pub spent: Amount,
382 pub unspent: Amount,
384 pub pending: Amount,
386}
387
388#[derive(Debug, Clone)]
396pub struct NUT13Options {
397 pub batch_size: u32,
399 pub max_gap: u32,
401}
402
403impl Default for NUT13Options {
404 fn default() -> Self {
405 Self {
406 batch_size: Self::DEFAULT_BATCH_SIZE,
407 max_gap: Self::DEFAULT_MAX_GAP,
408 }
409 }
410}
411
412impl NUT13Options {
413 pub const DEFAULT_BATCH_SIZE: u32 = 100;
415
416 pub const DEFAULT_MAX_GAP: u32 = 3;
418
419 pub fn new(batch_size: u32, max_gap: u32) -> Result<Self, Error> {
421 let opts = Self {
422 batch_size,
423 max_gap,
424 };
425 opts.validate()?;
426 Ok(opts)
427 }
428
429 pub(crate) fn validate(&self) -> Result<(), Error> {
430 if self.batch_size == 0 {
431 return Err(Error::InvalidNut13Options {
432 field: "batch_size",
433 reason: "must be greater than zero",
434 });
435 }
436
437 if self.max_gap == 0 {
438 return Err(Error::InvalidNut13Options {
439 field: "max_gap",
440 reason: "must be greater than zero",
441 });
442 }
443
444 Ok(())
445 }
446}
447
448#[derive(Clone, Default)]
450pub struct SendOptions {
451 pub memo: Option<SendMemo>,
453 pub conditions: Option<SpendingConditions>,
455 pub amount_split_target: SplitTarget,
457 pub send_kind: SendKind,
459 pub include_fee: bool,
461 pub max_proofs: Option<usize>,
463 pub metadata: HashMap<String, String>,
465 pub use_p2bk: bool,
467 pub p2pk_signing_keys: Vec<SecretKey>,
469 pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
471}
472
473impl fmt::Debug for SendOptions {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 f.debug_struct("SendOptions")
476 .field("memo", &self.memo)
477 .field("conditions", &self.conditions)
478 .field("amount_split_target", &self.amount_split_target)
479 .field("send_kind", &self.send_kind)
480 .field("include_fee", &self.include_fee)
481 .field("max_proofs", &self.max_proofs)
482 .field("metadata", &self.metadata)
483 .field("use_p2bk", &self.use_p2bk)
484 .field("p2pk_signing_keys", &"[redacted]")
485 .field(
486 "p2pk_locked_proof_send_mode",
487 &self.p2pk_locked_proof_send_mode,
488 )
489 .finish()
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
495pub enum P2PKLockedProofSendMode {
496 #[default]
498 Swap,
499 SignAndSend,
501}
502
503#[derive(Debug, Clone)]
505pub struct SendMemo {
506 pub memo: String,
508 pub include_memo: bool,
510}
511
512impl SendMemo {
513 pub fn for_token(memo: &str) -> Self {
515 Self {
516 memo: memo.to_string(),
517 include_memo: true,
518 }
519 }
520}
521
522#[derive(Clone, Default)]
524pub struct ReceiveOptions {
525 pub amount_split_target: SplitTarget,
527 pub p2pk_signing_keys: Vec<SecretKey>,
529 pub preimages: Vec<String>,
531 pub metadata: HashMap<String, String>,
533}
534
535impl fmt::Debug for ReceiveOptions {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.debug_struct("ReceiveOptions")
538 .field("amount_split_target", &self.amount_split_target)
539 .field("p2pk_signing_keys", &"[redacted]")
540 .field("preimages", &"[redacted]")
541 .field("metadata", &self.metadata)
542 .finish()
543 }
544}
545
546#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
548pub enum SendKind {
549 #[default]
550 OnlineExact,
552 OnlineTolerance(Amount),
554 OfflineExact,
556 OfflineTolerance(Amount),
558}
559
560impl SendKind {
561 pub fn is_online(&self) -> bool {
563 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
564 }
565
566 pub fn is_offline(&self) -> bool {
568 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
569 }
570
571 pub fn is_exact(&self) -> bool {
573 matches!(self, Self::OnlineExact | Self::OfflineExact)
574 }
575
576 pub fn has_tolerance(&self) -> bool {
578 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
579 }
580}
581
582#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
584pub struct Transaction {
585 pub mint_url: MintUrl,
587 pub direction: TransactionDirection,
589 pub amount: Amount,
591 pub fee: Amount,
593 pub unit: CurrencyUnit,
595 pub ys: Vec<PublicKey>,
597 pub timestamp: u64,
599 pub memo: Option<String>,
601 pub metadata: HashMap<String, String>,
603 pub quote_id: Option<String>,
605 pub payment_request: Option<String>,
607 #[serde(alias = "payment_preimage")]
609 pub payment_proof: Option<String>,
610 #[serde(default)]
612 pub payment_method: Option<PaymentMethod>,
613 #[serde(default)]
615 pub saga_id: Option<Uuid>,
616 #[serde(default)]
618 pub status: TransactionStatus,
619}
620
621impl fmt::Debug for Transaction {
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 f.debug_struct("Transaction")
624 .field("mint_url", &self.mint_url)
625 .field("direction", &self.direction)
626 .field("amount", &self.amount)
627 .field("fee", &self.fee)
628 .field("unit", &self.unit)
629 .field("ys", &self.ys)
630 .field("timestamp", &self.timestamp)
631 .field("memo", &self.memo)
632 .field("metadata", &self.metadata)
633 .field("quote_id", &self.quote_id)
634 .field("payment_request", &self.payment_request)
635 .field(
636 "payment_proof",
637 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
638 )
639 .field("payment_method", &self.payment_method)
640 .field("saga_id", &self.saga_id)
641 .field("status", &self.status)
642 .finish()
643 }
644}
645
646impl Transaction {
647 pub fn id(&self) -> TransactionId {
653 match self.saga_id {
654 Some(saga_id) => match self.metadata.get("batch_quote_id") {
655 Some(quote_id) => TransactionId::from_batch_quote(saga_id, quote_id),
656 None => TransactionId::from_saga_id(saga_id),
657 },
658 None => TransactionId::new(self.ys.clone()),
659 }
660 }
661
662 pub fn matches_conditions(
664 &self,
665 mint_url: &Option<MintUrl>,
666 direction: &Option<TransactionDirection>,
667 unit: &Option<CurrencyUnit>,
668 ) -> bool {
669 if let Some(mint_url) = mint_url {
670 if &self.mint_url != mint_url {
671 return false;
672 }
673 }
674 if let Some(direction) = direction {
675 if &self.direction != direction {
676 return false;
677 }
678 }
679 if let Some(unit) = unit {
680 if &self.unit != unit {
681 return false;
682 }
683 }
684 true
685 }
686}
687
688impl PartialOrd for Transaction {
689 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
690 Some(self.cmp(other))
691 }
692}
693
694impl Ord for Transaction {
695 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
696 self.timestamp
697 .cmp(&other.timestamp)
698 .reverse()
699 .then_with(|| self.id().cmp(&other.id()))
700 }
701}
702
703#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
705pub enum TransactionDirection {
706 Incoming,
708 Outgoing,
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
714#[serde(rename_all = "snake_case")]
715pub enum TransactionStatus {
716 Pending,
718 #[default]
720 Completed,
721 Failed,
723}
724
725impl fmt::Display for TransactionStatus {
726 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
727 match self {
728 Self::Pending => write!(f, "pending"),
729 Self::Completed => write!(f, "completed"),
730 Self::Failed => write!(f, "failed"),
731 }
732 }
733}
734
735impl FromStr for TransactionStatus {
736 type Err = Error;
737
738 fn from_str(value: &str) -> Result<Self, Self::Err> {
739 match value {
740 "pending" => Ok(Self::Pending),
741 "completed" => Ok(Self::Completed),
742 "failed" => Ok(Self::Failed),
743 _ => Err(Error::InvalidTransactionStatus),
744 }
745 }
746}
747
748impl std::fmt::Display for TransactionDirection {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 match self {
751 TransactionDirection::Incoming => write!(f, "Incoming"),
752 TransactionDirection::Outgoing => write!(f, "Outgoing"),
753 }
754 }
755}
756
757impl FromStr for TransactionDirection {
758 type Err = Error;
759
760 fn from_str(value: &str) -> Result<Self, Self::Err> {
761 match value {
762 "Incoming" => Ok(Self::Incoming),
763 "Outgoing" => Ok(Self::Outgoing),
764 _ => Err(Error::InvalidTransactionDirection),
765 }
766 }
767}
768
769#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
771#[serde(transparent)]
772pub struct TransactionId([u8; 32]);
773
774impl TransactionId {
775 pub fn new(ys: Vec<PublicKey>) -> Self {
779 let mut ys = ys;
780 ys.sort();
781 let mut hasher = sha256::Hash::engine();
782 for y in ys {
783 hasher.input(&y.to_bytes());
784 }
785 let hash = sha256::Hash::from_engine(hasher);
786 Self(hash.to_byte_array())
787 }
788
789 pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
793 let ys = proofs
794 .iter()
795 .map(|proof| proof.y())
796 .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
797 Ok(Self::new(ys))
798 }
799
800 pub fn from_saga_id(saga_id: Uuid) -> Self {
805 let mut bytes = [0_u8; 32];
806 let encoded = saga_id.simple().to_string();
807 for (destination, source) in bytes.iter_mut().zip(encoded.bytes()) {
808 *destination = source;
809 }
810 Self(bytes)
811 }
812
813 pub fn from_batch_quote(saga_id: Uuid, quote_id: &str) -> Self {
815 let mut hasher = sha256::Hash::engine();
816 hasher.input(saga_id.as_bytes());
817 hasher.input(quote_id.as_bytes());
818 Self(sha256::Hash::from_engine(hasher).to_byte_array())
819 }
820
821 pub fn from_bytes(bytes: [u8; 32]) -> Self {
823 Self(bytes)
824 }
825
826 pub fn from_hex(value: &str) -> Result<Self, Error> {
828 let bytes = hex::decode(value)?;
829 if bytes.len() != 32 {
830 return Err(Error::InvalidTransactionId);
831 }
832 let mut array = [0u8; 32];
833 array.copy_from_slice(&bytes);
834 Ok(Self(array))
835 }
836
837 pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
839 if slice.len() != 32 {
840 return Err(Error::InvalidTransactionId);
841 }
842 let mut array = [0u8; 32];
843 array.copy_from_slice(slice);
844 Ok(Self(array))
845 }
846
847 pub fn as_bytes(&self) -> &[u8; 32] {
849 &self.0
850 }
851
852 pub fn as_slice(&self) -> &[u8] {
854 &self.0
855 }
856}
857
858impl std::fmt::Display for TransactionId {
859 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 write!(f, "{}", hex::encode(self.0))
861 }
862}
863
864impl FromStr for TransactionId {
865 type Err = Error;
866
867 fn from_str(value: &str) -> Result<Self, Self::Err> {
868 Self::from_hex(value)
869 }
870}
871
872impl TryFrom<Proofs> for TransactionId {
873 type Error = nut00::Error;
874
875 fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
876 Self::from_proofs(proofs)
877 }
878}
879
880#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
882#[serde(rename_all = "snake_case")]
883pub enum OperationKind {
884 Send,
886 Receive,
888 Swap,
890 Mint,
892 Melt,
894}
895
896impl fmt::Display for OperationKind {
897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898 match self {
899 OperationKind::Send => write!(f, "send"),
900 OperationKind::Receive => write!(f, "receive"),
901 OperationKind::Swap => write!(f, "swap"),
902 OperationKind::Mint => write!(f, "mint"),
903 OperationKind::Melt => write!(f, "melt"),
904 }
905 }
906}
907
908impl FromStr for OperationKind {
909 type Err = Error;
910
911 fn from_str(s: &str) -> Result<Self, Self::Err> {
912 match s {
913 "send" => Ok(OperationKind::Send),
914 "receive" => Ok(OperationKind::Receive),
915 "swap" => Ok(OperationKind::Swap),
916 "mint" => Ok(OperationKind::Mint),
917 "melt" => Ok(OperationKind::Melt),
918 _ => Err(Error::InvalidOperationKind),
919 }
920 }
921}
922
923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
925pub enum KeysetFilter {
926 Active,
928 All,
930}
931
932#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
937pub enum KeysetLoadPolicy {
938 CacheOnly,
941 #[default]
947 CacheThenNetwork,
948 Refresh,
952}
953
954#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
963#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
964pub trait Wallet: Send + Sync {
965 type Error: std::error::Error + Send + Sync + 'static;
967 type Amount: Clone + Send + Sync;
969 type MintUrl: Clone + Send + Sync;
971 type CurrencyUnit: Clone + Send + Sync;
973 type MintInfo: Clone + Send + Sync;
975 type KeySetInfo: Clone + Send + Sync;
977 type MintQuote: Clone + Send + Sync;
979 type MeltQuote: Clone + Send + Sync;
981 type CrossMintTransferQuote: Clone + Send + Sync;
983 type PaymentMethod: Clone + Send + Sync;
985 type MeltOptions: Clone + Send + Sync;
987 type OperationId: Clone + Send + Sync;
989 type PreparedSend<'a>: Send + Sync
991 where
992 Self: 'a;
993 type PreparedMelt<'a>: Send + Sync
995 where
996 Self: 'a;
997 type Subscription: Send + Sync;
999 type SubscribeParams: Clone + Send + Sync;
1001 type RecoveryReport: Clone + Send + Sync;
1003
1004 fn mint_url(&self) -> Self::MintUrl;
1006
1007 fn unit(&self) -> Self::CurrencyUnit;
1009
1010 async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
1012
1013 async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
1015
1016 async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
1018
1019 async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
1021
1022 async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
1024
1025 async fn keysets(&self, policy: KeysetLoadPolicy)
1033 -> Result<Vec<Self::KeySetInfo>, Self::Error>;
1034
1035 async fn active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
1040
1041 async fn keyset(&self, keyset_id: Id) -> Result<Self::KeySetInfo, Self::Error>;
1043
1044 async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
1046
1047 async fn get_keyset_count_fee(
1049 &self,
1050 keyset_id: &Id,
1051 count: u64,
1052 ) -> Result<Self::Amount, Self::Error>;
1053
1054 async fn get_keyset_fees_and_amounts_by_id(
1056 &self,
1057 keyset_id: Id,
1058 ) -> Result<FeeAndAmounts, Self::Error>;
1059
1060 async fn mint_quote(
1062 &self,
1063 method: Self::PaymentMethod,
1064 amount: Option<Self::Amount>,
1065 description: Option<String>,
1066 extra: Option<String>,
1067 ) -> Result<Self::MintQuote, Self::Error>;
1068
1069 async fn melt_quote(
1071 &self,
1072 method: Self::PaymentMethod,
1073 request: String,
1074 options: Option<Self::MeltOptions>,
1075 extra: Option<String>,
1076 ) -> Result<Self::MeltQuote, Self::Error>;
1077
1078 async fn cross_mint_transfer_quote_max(
1087 &self,
1088 target_wallet: &Self,
1089 ) -> Result<Self::CrossMintTransferQuote, Self::Error>;
1090
1091 async fn list_transactions(
1093 &self,
1094 direction: Option<TransactionDirection>,
1095 ) -> Result<Vec<Transaction>, Self::Error>;
1096
1097 async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
1099
1100 async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
1102
1103 async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
1105
1106 async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
1108
1109 async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
1111
1112 async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
1114
1115 async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
1117
1118 async fn calculate_fee(
1120 &self,
1121 proof_count: u64,
1122 keyset_id: Id,
1123 ) -> Result<Self::Amount, Self::Error>;
1124
1125 async fn receive(
1127 &self,
1128 encoded_token: &str,
1129 options: ReceiveOptions,
1130 ) -> Result<Self::Amount, Self::Error>;
1131
1132 async fn receive_proofs(
1134 &self,
1135 proofs: Proofs,
1136 options: ReceiveOptions,
1137 memo: Option<String>,
1138 token: Option<String>,
1139 ) -> Result<Self::Amount, Self::Error>;
1140
1141 async fn prepare_send(
1143 &self,
1144 amount: Self::Amount,
1145 options: SendOptions,
1146 ) -> Result<Self::PreparedSend<'_>, Self::Error>;
1147
1148 async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
1150
1151 async fn revoke_send(
1153 &self,
1154 operation_id: Self::OperationId,
1155 ) -> Result<Self::Amount, Self::Error>;
1156
1157 async fn check_send_status(&self, operation_id: Self::OperationId)
1159 -> Result<bool, Self::Error>;
1160
1161 async fn mint(
1163 &self,
1164 quote_id: &str,
1165 split_target: SplitTarget,
1166 spending_conditions: Option<SpendingConditions>,
1167 ) -> Result<Proofs, Self::Error>;
1168
1169 async fn mint_unissued_quotes(&self) -> Result<Self::Amount, Self::Error>;
1171
1172 async fn check_mint_quote_status(&self, quote_id: &str)
1174 -> Result<Self::MintQuote, Self::Error>;
1175
1176 async fn fetch_mint_quote(
1178 &self,
1179 quote_id: &str,
1180 payment_method: Option<Self::PaymentMethod>,
1181 ) -> Result<Self::MintQuote, Self::Error>;
1182
1183 async fn prepare_melt(
1185 &self,
1186 quote_id: &str,
1187 metadata: HashMap<String, String>,
1188 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1189
1190 async fn prepare_melt_proofs(
1192 &self,
1193 quote_id: &str,
1194 proofs: Proofs,
1195 metadata: HashMap<String, String>,
1196 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1197
1198 async fn prepare_melt_token(
1204 &self,
1205 quote_id: &str,
1206 encoded_token: &str,
1207 metadata: HashMap<String, String>,
1208 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1209
1210 async fn swap(
1212 &self,
1213 amount: Option<Self::Amount>,
1214 split_target: SplitTarget,
1215 input_proofs: Proofs,
1216 spending_conditions: Option<SpendingConditions>,
1217 include_fees: bool,
1218 use_p2bk: bool,
1219 ) -> Result<Option<Proofs>, Self::Error>;
1220
1221 async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
1223
1224 async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
1226
1227 async fn refresh_access_token(&self) -> Result<(), Self::Error>;
1229
1230 async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
1232
1233 async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
1235
1236 async fn restore(&self) -> Result<Restored, Self::Error>;
1238
1239 async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, Self::Error>;
1241
1242 async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
1244
1245 async fn subscribe_mint_quote_state(
1250 &self,
1251 quote_ids: Vec<String>,
1252 method: Self::PaymentMethod,
1253 ) -> Result<Self::Subscription, Self::Error>;
1254
1255 fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
1261
1262 #[cfg(feature = "http")]
1268 fn set_rate_limiting_config(&self, config: Option<RateLimitConfig>);
1269
1270 #[cfg(feature = "http")]
1275 fn is_rate_limited(&self) -> bool;
1276
1277 #[cfg(feature = "http")]
1283 async fn flush_rate_limits(&self);
1284
1285 async fn subscribe(
1287 &self,
1288 params: Self::SubscribeParams,
1289 ) -> Result<Self::Subscription, Self::Error>;
1290
1291 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1293 async fn melt_bip353_quote(
1294 &self,
1295 bip353_address: &str,
1296 amount_msat: Self::Amount,
1297 network: bitcoin::Network,
1298 ) -> Result<Self::MeltQuote, Self::Error>;
1299
1300 #[cfg(not(target_arch = "wasm32"))]
1302 async fn melt_lightning_address_quote(
1303 &self,
1304 lightning_address: &str,
1305 amount_msat: Self::Amount,
1306 ) -> Result<Self::MeltQuote, Self::Error>;
1307
1308 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1314 async fn melt_human_readable_quote(
1315 &self,
1316 address: &str,
1317 amount_msat: Self::Amount,
1318 network: bitcoin::Network,
1319 ) -> Result<Self::MeltQuote, Self::Error>;
1320
1321 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1323 async fn melt_human_readable(
1324 &self,
1325 address: &str,
1326 amount_msat: Self::Amount,
1327 network: bitcoin::Network,
1328 ) -> Result<Self::MeltQuote, Self::Error> {
1329 self.melt_human_readable_quote(address, amount_msat, network)
1330 .await
1331 }
1332
1333 async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1335 self.check_mint_quote_status(quote_id).await
1336 }
1337
1338 async fn mint_unified(
1340 &self,
1341 quote_id: &str,
1342 split_target: SplitTarget,
1343 spending_conditions: Option<SpendingConditions>,
1344 ) -> Result<Proofs, Self::Error> {
1345 self.mint(quote_id, split_target, spending_conditions).await
1346 }
1347
1348 async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1354
1355 async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1358
1359 async fn get_public_key(
1361 &self,
1362 pubkey: &PublicKey,
1363 ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1364
1365 async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1367
1368 async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1370
1371 async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1373}
1374
1375#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1377pub struct P2PKSigningKey {
1378 pub pubkey: PublicKey,
1380 pub derivation_path: DerivationPath,
1382 pub derivation_index: u32,
1384 pub created_time: u64,
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390 use super::*;
1391 use crate::nuts::Id;
1392 use crate::secret::Secret;
1393
1394 #[test]
1395 fn test_transaction_id_from_hex() {
1396 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1397 let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1398 assert_eq!(transaction_id.to_string(), hex_str);
1399 }
1400
1401 #[test]
1402 fn test_transaction_id_from_hex_empty_string() {
1403 let hex_str = "";
1404 let res = TransactionId::from_hex(hex_str);
1405 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1406 }
1407
1408 #[test]
1409 fn test_transaction_id_from_hex_longer_string() {
1410 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1411 let res = TransactionId::from_hex(hex_str);
1412 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1413 }
1414
1415 #[test]
1416 fn transaction_id_from_saga_id_uses_canonical_uuid_bytes() {
1417 let saga_id =
1418 Uuid::parse_str("019fa338-b72f-7f21-9bb2-a504cdd5927b").expect("valid saga ID");
1419 let transaction_id = TransactionId::from_saga_id(saga_id);
1420
1421 assert_eq!(
1422 transaction_id.as_bytes(),
1423 b"019fa338b72f7f219bb2a504cdd5927b"
1424 );
1425 }
1426
1427 #[test]
1428 fn batch_quote_transaction_ids_are_stable_and_distinct() {
1429 let saga_id =
1430 Uuid::parse_str("019fa338-b72f-7f21-9bb2-a504cdd5927b").expect("valid saga ID");
1431
1432 let first = TransactionId::from_batch_quote(saga_id, "quote-a");
1433 let first_again = TransactionId::from_batch_quote(saga_id, "quote-a");
1434 let second = TransactionId::from_batch_quote(saga_id, "quote-b");
1435
1436 assert_eq!(first, first_again);
1437 assert_ne!(first, second);
1438 assert_ne!(first, TransactionId::from_saga_id(saga_id));
1439 }
1440
1441 #[test]
1442 fn saga_managed_transactions_with_the_same_ys_have_distinct_ids() {
1443 let ys = vec![SecretKey::generate().public_key()];
1444 let transaction = Transaction {
1445 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1446 direction: TransactionDirection::Outgoing,
1447 amount: Amount::from(10),
1448 fee: Amount::ZERO,
1449 unit: CurrencyUnit::Sat,
1450 ys,
1451 timestamp: 42,
1452 memo: None,
1453 metadata: HashMap::new(),
1454 quote_id: None,
1455 payment_request: None,
1456 payment_proof: None,
1457 payment_method: None,
1458 saga_id: Some(Uuid::new_v4()),
1459 status: TransactionStatus::Pending,
1460 };
1461 let mut received = transaction.clone();
1462 received.direction = TransactionDirection::Incoming;
1463 received.saga_id = Some(Uuid::new_v4());
1464
1465 assert_ne!(transaction.id(), received.id());
1466 }
1467
1468 #[test]
1469 fn test_matches_conditions() {
1470 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1471 let proof = Proof::new(
1472 Amount::from(64),
1473 keyset_id,
1474 Secret::new("test_secret"),
1475 PublicKey::from_hex(
1476 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1477 )
1478 .unwrap(),
1479 );
1480
1481 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1482 let proof_info =
1483 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1484
1485 assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1487 assert!(!proof_info.matches_conditions(
1488 &Some(MintUrl::from_str("https://different.com").unwrap()),
1489 &None,
1490 &None,
1491 &None
1492 ));
1493
1494 assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1496 assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1497
1498 assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1500 assert!(proof_info.matches_conditions(
1501 &None,
1502 &None,
1503 &Some(vec![State::Unspent, State::Spent]),
1504 &None
1505 ));
1506 assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1507
1508 assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1510
1511 assert!(proof_info.matches_conditions(
1513 &Some(mint_url),
1514 &Some(CurrencyUnit::Sat),
1515 &Some(vec![State::Unspent]),
1516 &None
1517 ));
1518 }
1519
1520 #[test]
1521 fn test_matches_conditions_with_spending_conditions() {
1522 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1527 let proof = Proof::new(
1528 Amount::from(64),
1529 keyset_id,
1530 Secret::new("test_secret"),
1531 PublicKey::from_hex(
1532 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1533 )
1534 .unwrap(),
1535 );
1536
1537 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1538 let proof_info =
1539 ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1540
1541 assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1543
1544 let dummy_condition = SpendingConditions::P2PKConditions {
1546 data: SecretKey::generate().public_key(),
1547 conditions: None,
1548 };
1549 assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1550 }
1551
1552 #[test]
1553 fn wallet_record_debug_redacts_spendable_secrets() {
1554 let proof_secret = "wallet-proof-secret";
1555 let payment_proof = "wallet-payment-preimage";
1556 let keyset_id = Id::from_str("00deadbeef123456").expect("valid keyset ID");
1557 let proof = Proof::new(
1558 Amount::from(64),
1559 keyset_id,
1560 Secret::new(proof_secret),
1561 PublicKey::from_hex(
1562 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1563 )
1564 .expect("valid public key"),
1565 );
1566 let mint_url = MintUrl::from_str("https://mint.example.com").expect("valid mint URL");
1567 let proof_info = ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat)
1568 .expect("valid proof");
1569 let melt_quote = MeltQuote {
1570 id: "public-quote-id".to_string(),
1571 mint_url: Some(mint_url.clone()),
1572 unit: CurrencyUnit::Sat,
1573 amount: Amount::from(10),
1574 request: "public-payment-request".to_string(),
1575 fee_reserve: Amount::from(1),
1576 state: MeltQuoteState::Paid,
1577 expiry: 1_000,
1578 payment_proof: Some(payment_proof.to_string()),
1579 estimated_blocks: None,
1580 fee_index: None,
1581 payment_method: PaymentMethod::BOLT11,
1582 used_by_operation: None,
1583 version: 0,
1584 };
1585 let transaction = Transaction {
1586 mint_url,
1587 direction: TransactionDirection::Outgoing,
1588 amount: Amount::from(10),
1589 fee: Amount::from(1),
1590 unit: CurrencyUnit::Sat,
1591 ys: vec![],
1592 timestamp: 42,
1593 memo: None,
1594 metadata: HashMap::new(),
1595 quote_id: Some("public-quote-id".to_string()),
1596 payment_request: None,
1597 payment_proof: Some(payment_proof.to_string()),
1598 payment_method: Some(PaymentMethod::BOLT11),
1599 saga_id: None,
1600 status: TransactionStatus::Completed,
1601 };
1602
1603 for debug in [
1604 format!("{proof_info:?}"),
1605 format!("{melt_quote:?}"),
1606 format!("{transaction:?}"),
1607 ] {
1608 assert!(debug.contains("[REDACTED]"));
1609 assert!(!debug.contains(proof_secret));
1610 assert!(!debug.contains(payment_proof));
1611 }
1612 }
1613
1614 #[test]
1615 fn test_wallet_options_debug_redacts_p2pk_signing_keys() {
1616 let secret_key = SecretKey::generate();
1617 let secret_hex = secret_key.to_secret_hex();
1618 let preimage = "super_secret_htlc_preimage_xyz";
1619
1620 let send_options = SendOptions {
1621 p2pk_signing_keys: vec![secret_key.clone()],
1622 ..Default::default()
1623 };
1624 let receive_options = ReceiveOptions {
1625 p2pk_signing_keys: vec![secret_key],
1626 preimages: vec![preimage.to_string()],
1627 ..Default::default()
1628 };
1629
1630 let send_debug = format!("{:?}", send_options);
1631 let receive_debug = format!("{:?}", receive_options);
1632
1633 assert!(!send_debug.contains(&secret_hex));
1634 assert!(send_debug.contains("[redacted]"));
1635 assert!(!receive_debug.contains(&secret_hex));
1636 assert!(!receive_debug.contains(preimage));
1637 assert!(receive_debug.contains("[redacted]"));
1638 }
1639
1640 #[test]
1641 fn nut13_options_defaults_match_nut13_spec() {
1642 let opts = NUT13Options::default();
1645 assert_eq!(opts.batch_size, NUT13Options::DEFAULT_BATCH_SIZE);
1646 assert_eq!(opts.max_gap, NUT13Options::DEFAULT_MAX_GAP);
1647 }
1648
1649 #[test]
1650 fn nut13_options_new_accepts_custom_values() {
1651 let opts = NUT13Options::new(25, 2).unwrap();
1652 let cloned = opts.clone();
1653 assert_eq!(cloned.batch_size, 25);
1654 assert_eq!(cloned.max_gap, 2);
1655 }
1656
1657 #[test]
1658 fn nut13_options_reject_zero_batch_size() {
1659 let err = NUT13Options::new(0, 2).unwrap_err();
1660 assert!(matches!(
1661 err,
1662 Error::InvalidNut13Options {
1663 field: "batch_size",
1664 ..
1665 }
1666 ));
1667 }
1668
1669 #[test]
1670 fn nut13_options_reject_zero_max_gap() {
1671 let err = NUT13Options::new(25, 0).unwrap_err();
1672 assert!(matches!(
1673 err,
1674 Error::InvalidNut13Options {
1675 field: "max_gap",
1676 ..
1677 }
1678 ));
1679 }
1680
1681 #[test]
1682 fn transaction_status_round_trips_and_rejects_unknown_values() {
1683 for status in [
1684 TransactionStatus::Pending,
1685 TransactionStatus::Completed,
1686 TransactionStatus::Failed,
1687 ] {
1688 assert_eq!(
1689 TransactionStatus::from_str(&status.to_string()).expect("valid status"),
1690 status
1691 );
1692 }
1693
1694 assert!(matches!(
1695 TransactionStatus::from_str("unknown"),
1696 Err(Error::InvalidTransactionStatus)
1697 ));
1698 }
1699
1700 #[test]
1701 fn transaction_without_status_defaults_to_completed() {
1702 let transaction = Transaction {
1703 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1704 direction: TransactionDirection::Incoming,
1705 amount: Amount::from(10),
1706 fee: Amount::ZERO,
1707 unit: CurrencyUnit::Sat,
1708 ys: vec![SecretKey::generate().public_key()],
1709 timestamp: 42,
1710 memo: None,
1711 metadata: HashMap::new(),
1712 quote_id: None,
1713 payment_request: None,
1714 payment_proof: None,
1715 payment_method: None,
1716 saga_id: None,
1717 status: TransactionStatus::Pending,
1718 };
1719 let mut value = serde_json::to_value(transaction).expect("serialize transaction");
1720 value
1721 .as_object_mut()
1722 .expect("transaction serializes as an object")
1723 .remove("status");
1724
1725 let decoded: Transaction =
1726 serde_json::from_value(value).expect("deserialize legacy transaction");
1727
1728 assert_eq!(decoded.status, TransactionStatus::Completed);
1729 }
1730}