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 derivation_index: Option<u32>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub used_by_operation: Option<Uuid>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub created_by_operation: Option<Uuid>,
81}
82
83impl fmt::Debug for ProofInfo {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.debug_struct("ProofInfo")
86 .field("amount", &self.proof.amount)
87 .field("keyset_id", &self.proof.keyset_id)
88 .field("proof", &"[REDACTED]")
89 .field("y", &self.y)
90 .field("mint_url", &self.mint_url)
91 .field("state", &self.state)
92 .field("spending_condition", &self.spending_condition)
93 .field("unit", &self.unit)
94 .field("used_by_operation", &self.used_by_operation)
95 .field("created_by_operation", &self.created_by_operation)
96 .finish()
97 }
98}
99
100impl ProofInfo {
101 pub fn new(
103 proof: Proof,
104 mint_url: MintUrl,
105 state: State,
106 unit: CurrencyUnit,
107 ) -> Result<Self, Error> {
108 let y = proof.y()?;
109
110 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
111
112 Ok(Self {
113 proof,
114 y,
115 mint_url,
116 state,
117 spending_condition,
118 unit,
119 derivation_index: None,
120 used_by_operation: None,
121 created_by_operation: None,
122 })
123 }
124
125 pub fn new_with_operations(
127 proof: Proof,
128 mint_url: MintUrl,
129 state: State,
130 unit: CurrencyUnit,
131 used_by_operation: Option<Uuid>,
132 created_by_operation: Option<Uuid>,
133 ) -> Result<Self, Error> {
134 let y = proof.y()?;
135
136 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
137
138 Ok(Self {
139 proof,
140 y,
141 mint_url,
142 state,
143 spending_condition,
144 unit,
145 derivation_index: None,
146 used_by_operation,
147 created_by_operation,
148 })
149 }
150
151 pub fn with_derivation_index(mut self, derivation_index: u32) -> Self {
153 self.derivation_index = Some(derivation_index);
154 self
155 }
156
157 pub fn matches_conditions(
159 &self,
160 mint_url: &Option<MintUrl>,
161 unit: &Option<CurrencyUnit>,
162 state: &Option<Vec<State>>,
163 spending_conditions: &Option<Vec<SpendingConditions>>,
164 ) -> bool {
165 if let Some(mint_url) = mint_url {
166 if mint_url.ne(&self.mint_url) {
167 return false;
168 }
169 }
170
171 if let Some(unit) = unit {
172 if unit.ne(&self.unit) {
173 return false;
174 }
175 }
176
177 if let Some(state) = state {
178 if !state.contains(&self.state) {
179 return false;
180 }
181 }
182
183 if let Some(spending_conditions) = spending_conditions {
184 match &self.spending_condition {
185 None => {
186 if !spending_conditions.is_empty() {
187 return false;
188 }
189 }
190 Some(s) => {
191 if !spending_conditions.contains(s) {
192 return false;
193 }
194 }
195 }
196 }
197
198 true
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct MintQuote {
205 pub id: String,
207 pub mint_url: MintUrl,
209 pub payment_method: PaymentMethod,
211 pub amount: Option<Amount>,
216 pub unit: CurrencyUnit,
218 pub request: String,
220 pub state: MintQuoteState,
222 pub expiry: u64,
224 pub secret_key: Option<SecretKey>,
226 #[serde(default)]
228 pub amount_issued: Amount,
229 #[serde(default)]
231 pub amount_paid: Amount,
232 #[serde(default)]
234 pub updated_at: u64,
235 pub estimated_blocks: Option<u32>,
237 #[serde(default)]
239 pub used_by_operation: Option<String>,
240 #[serde(default)]
242 pub version: u32,
243}
244
245#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
247pub struct MeltQuote {
248 pub id: String,
250 pub mint_url: Option<MintUrl>,
252 pub unit: CurrencyUnit,
254 pub amount: Amount,
256 pub request: String,
258 pub fee_reserve: Amount,
260 pub state: MeltQuoteState,
262 pub expiry: u64,
264 #[serde(alias = "payment_preimage")]
266 pub payment_proof: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub estimated_blocks: Option<u32>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub fee_index: Option<u32>,
273 pub payment_method: PaymentMethod,
275 #[serde(default)]
277 pub used_by_operation: Option<String>,
278 #[serde(default)]
280 pub version: u32,
281}
282
283impl fmt::Debug for MeltQuote {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 f.debug_struct("MeltQuote")
286 .field("id", &self.id)
287 .field("mint_url", &self.mint_url)
288 .field("unit", &self.unit)
289 .field("amount", &self.amount)
290 .field("request", &self.request)
291 .field("fee_reserve", &self.fee_reserve)
292 .field("state", &self.state)
293 .field("expiry", &self.expiry)
294 .field(
295 "payment_proof",
296 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
297 )
298 .field("estimated_blocks", &self.estimated_blocks)
299 .field("fee_index", &self.fee_index)
300 .field("payment_method", &self.payment_method)
301 .field("used_by_operation", &self.used_by_operation)
302 .field("version", &self.version)
303 .finish()
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct CrossMintTransferQuote {
310 pub mint_quote: MintQuote,
312 pub melt_quote: MeltQuote,
314 pub input_fee: Amount,
316}
317
318impl MintQuote {
319 #[allow(clippy::too_many_arguments)]
321 pub fn new(
322 id: String,
323 mint_url: MintUrl,
324 payment_method: PaymentMethod,
325 amount: Option<Amount>,
326 unit: CurrencyUnit,
327 request: String,
328 expiry: u64,
329 secret_key: Option<SecretKey>,
330 ) -> Self {
331 Self {
332 id,
333 mint_url,
334 payment_method,
335 amount,
336 unit,
337 request,
338 state: MintQuoteState::Unpaid,
339 expiry,
340 secret_key,
341 amount_issued: Amount::ZERO,
342 amount_paid: Amount::ZERO,
343 updated_at: 0,
344 estimated_blocks: None,
345 used_by_operation: None,
346 version: 0,
347 }
348 }
349
350 pub fn total_amount(&self) -> Amount {
352 self.amount_paid
353 }
354
355 pub fn state_from_amounts(&self) -> MintQuoteState {
357 quote_state_from_amounts(self.amount_paid, self.amount_issued).unwrap_or(self.state)
358 }
359
360 pub fn update_state_from_amounts(&mut self) {
362 self.state = self.state_from_amounts();
363 }
364
365 pub fn is_expired(&self, current_time: u64) -> bool {
367 current_time > self.expiry
368 }
369
370 pub fn amount_mintable(&self) -> Amount {
372 if self.payment_method == PaymentMethod::BOLT11 {
373 if self.state == MintQuoteState::Paid {
375 self.amount.unwrap_or(Amount::ZERO)
376 } else {
377 Amount::ZERO
378 }
379 } else {
380 self.amount_paid
382 .checked_sub(self.amount_issued)
383 .unwrap_or(Amount::ZERO)
384 }
385 }
386}
387
388#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
390pub struct Restored {
391 pub spent: Amount,
393 pub unspent: Amount,
395 pub pending: Amount,
397}
398
399#[derive(Debug, Clone)]
407pub struct NUT13Options {
408 pub batch_size: u32,
410 pub max_gap: u32,
412}
413
414impl Default for NUT13Options {
415 fn default() -> Self {
416 Self {
417 batch_size: Self::DEFAULT_BATCH_SIZE,
418 max_gap: Self::DEFAULT_MAX_GAP,
419 }
420 }
421}
422
423impl NUT13Options {
424 pub const DEFAULT_BATCH_SIZE: u32 = 100;
426
427 pub const DEFAULT_MAX_GAP: u32 = 3;
429
430 pub fn new(batch_size: u32, max_gap: u32) -> Result<Self, Error> {
432 let opts = Self {
433 batch_size,
434 max_gap,
435 };
436 opts.validate()?;
437 Ok(opts)
438 }
439
440 pub(crate) fn validate(&self) -> Result<(), Error> {
441 if self.batch_size == 0 {
442 return Err(Error::InvalidNut13Options {
443 field: "batch_size",
444 reason: "must be greater than zero",
445 });
446 }
447
448 if self.max_gap == 0 {
449 return Err(Error::InvalidNut13Options {
450 field: "max_gap",
451 reason: "must be greater than zero",
452 });
453 }
454
455 Ok(())
456 }
457}
458
459#[derive(Clone, Default)]
461pub struct SendOptions {
462 pub memo: Option<SendMemo>,
464 pub conditions: Option<SpendingConditions>,
466 pub amount_split_target: SplitTarget,
468 pub send_kind: SendKind,
470 pub include_fee: bool,
472 pub max_proofs: Option<usize>,
474 pub metadata: HashMap<String, String>,
476 pub use_p2bk: bool,
478 pub p2pk_signing_keys: Vec<SecretKey>,
480 pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
482}
483
484impl fmt::Debug for SendOptions {
485 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486 f.debug_struct("SendOptions")
487 .field("memo", &self.memo)
488 .field("conditions", &self.conditions)
489 .field("amount_split_target", &self.amount_split_target)
490 .field("send_kind", &self.send_kind)
491 .field("include_fee", &self.include_fee)
492 .field("max_proofs", &self.max_proofs)
493 .field("metadata", &self.metadata)
494 .field("use_p2bk", &self.use_p2bk)
495 .field("p2pk_signing_keys", &"[redacted]")
496 .field(
497 "p2pk_locked_proof_send_mode",
498 &self.p2pk_locked_proof_send_mode,
499 )
500 .finish()
501 }
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
506pub enum P2PKLockedProofSendMode {
507 #[default]
509 Swap,
510 SignAndSend,
512}
513
514#[derive(Debug, Clone)]
516pub struct SendMemo {
517 pub memo: String,
519 pub include_memo: bool,
521}
522
523impl SendMemo {
524 pub fn for_token(memo: &str) -> Self {
526 Self {
527 memo: memo.to_string(),
528 include_memo: true,
529 }
530 }
531}
532
533#[derive(Clone, Default)]
535pub struct ReceiveOptions {
536 pub amount_split_target: SplitTarget,
538 pub p2pk_signing_keys: Vec<SecretKey>,
540 pub preimages: Vec<String>,
542 pub metadata: HashMap<String, String>,
544}
545
546impl fmt::Debug for ReceiveOptions {
547 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548 f.debug_struct("ReceiveOptions")
549 .field("amount_split_target", &self.amount_split_target)
550 .field("p2pk_signing_keys", &"[redacted]")
551 .field("preimages", &"[redacted]")
552 .field("metadata", &self.metadata)
553 .finish()
554 }
555}
556
557#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
559pub enum SendKind {
560 #[default]
561 OnlineExact,
563 OnlineTolerance(Amount),
565 OfflineExact,
567 OfflineTolerance(Amount),
569}
570
571impl SendKind {
572 pub fn is_online(&self) -> bool {
574 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
575 }
576
577 pub fn is_offline(&self) -> bool {
579 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
580 }
581
582 pub fn is_exact(&self) -> bool {
584 matches!(self, Self::OnlineExact | Self::OfflineExact)
585 }
586
587 pub fn has_tolerance(&self) -> bool {
589 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
590 }
591}
592
593#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
595pub struct Transaction {
596 pub mint_url: MintUrl,
598 pub direction: TransactionDirection,
600 pub amount: Amount,
602 pub fee: Amount,
604 pub unit: CurrencyUnit,
606 pub ys: Vec<PublicKey>,
608 pub timestamp: u64,
610 pub memo: Option<String>,
612 pub metadata: HashMap<String, String>,
614 pub quote_id: Option<String>,
616 pub payment_request: Option<String>,
618 #[serde(alias = "payment_preimage")]
620 pub payment_proof: Option<String>,
621 #[serde(default)]
623 pub payment_method: Option<PaymentMethod>,
624 #[serde(default)]
626 pub saga_id: Option<Uuid>,
627 #[serde(default)]
629 pub status: TransactionStatus,
630}
631
632impl fmt::Debug for Transaction {
633 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634 f.debug_struct("Transaction")
635 .field("mint_url", &self.mint_url)
636 .field("direction", &self.direction)
637 .field("amount", &self.amount)
638 .field("fee", &self.fee)
639 .field("unit", &self.unit)
640 .field("ys", &self.ys)
641 .field("timestamp", &self.timestamp)
642 .field("memo", &self.memo)
643 .field("metadata", &self.metadata)
644 .field("quote_id", &self.quote_id)
645 .field("payment_request", &self.payment_request)
646 .field(
647 "payment_proof",
648 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
649 )
650 .field("payment_method", &self.payment_method)
651 .field("saga_id", &self.saga_id)
652 .field("status", &self.status)
653 .finish()
654 }
655}
656
657impl Transaction {
658 pub fn id(&self) -> TransactionId {
664 match self.saga_id {
665 Some(saga_id) => match self.metadata.get("batch_quote_id") {
666 Some(quote_id) => TransactionId::from_batch_quote(saga_id, quote_id),
667 None => TransactionId::from_saga_id(saga_id),
668 },
669 None => TransactionId::new(self.ys.clone()),
670 }
671 }
672
673 pub fn matches_conditions(
675 &self,
676 mint_url: &Option<MintUrl>,
677 direction: &Option<TransactionDirection>,
678 unit: &Option<CurrencyUnit>,
679 ) -> bool {
680 if let Some(mint_url) = mint_url {
681 if &self.mint_url != mint_url {
682 return false;
683 }
684 }
685 if let Some(direction) = direction {
686 if &self.direction != direction {
687 return false;
688 }
689 }
690 if let Some(unit) = unit {
691 if &self.unit != unit {
692 return false;
693 }
694 }
695 true
696 }
697}
698
699impl PartialOrd for Transaction {
700 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
701 Some(self.cmp(other))
702 }
703}
704
705impl Ord for Transaction {
706 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
707 self.timestamp
708 .cmp(&other.timestamp)
709 .reverse()
710 .then_with(|| self.id().cmp(&other.id()))
711 }
712}
713
714#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
716pub enum TransactionDirection {
717 Incoming,
719 Outgoing,
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
725#[serde(rename_all = "snake_case")]
726pub enum TransactionStatus {
727 Pending,
729 #[default]
731 Completed,
732 Failed,
734}
735
736impl fmt::Display for TransactionStatus {
737 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738 match self {
739 Self::Pending => write!(f, "pending"),
740 Self::Completed => write!(f, "completed"),
741 Self::Failed => write!(f, "failed"),
742 }
743 }
744}
745
746impl FromStr for TransactionStatus {
747 type Err = Error;
748
749 fn from_str(value: &str) -> Result<Self, Self::Err> {
750 match value {
751 "pending" => Ok(Self::Pending),
752 "completed" => Ok(Self::Completed),
753 "failed" => Ok(Self::Failed),
754 _ => Err(Error::InvalidTransactionStatus),
755 }
756 }
757}
758
759impl std::fmt::Display for TransactionDirection {
760 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
761 match self {
762 TransactionDirection::Incoming => write!(f, "Incoming"),
763 TransactionDirection::Outgoing => write!(f, "Outgoing"),
764 }
765 }
766}
767
768impl FromStr for TransactionDirection {
769 type Err = Error;
770
771 fn from_str(value: &str) -> Result<Self, Self::Err> {
772 match value {
773 "Incoming" => Ok(Self::Incoming),
774 "Outgoing" => Ok(Self::Outgoing),
775 _ => Err(Error::InvalidTransactionDirection),
776 }
777 }
778}
779
780#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
782#[serde(transparent)]
783pub struct TransactionId([u8; 32]);
784
785impl TransactionId {
786 pub fn new(ys: Vec<PublicKey>) -> Self {
790 let mut ys = ys;
791 ys.sort();
792 let mut hasher = sha256::Hash::engine();
793 for y in ys {
794 hasher.input(&y.to_bytes());
795 }
796 let hash = sha256::Hash::from_engine(hasher);
797 Self(hash.to_byte_array())
798 }
799
800 pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
804 let ys = proofs
805 .iter()
806 .map(|proof| proof.y())
807 .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
808 Ok(Self::new(ys))
809 }
810
811 pub fn from_saga_id(saga_id: Uuid) -> Self {
816 let mut bytes = [0_u8; 32];
817 let encoded = saga_id.simple().to_string();
818 for (destination, source) in bytes.iter_mut().zip(encoded.bytes()) {
819 *destination = source;
820 }
821 Self(bytes)
822 }
823
824 pub fn from_batch_quote(saga_id: Uuid, quote_id: &str) -> Self {
826 let mut hasher = sha256::Hash::engine();
827 hasher.input(saga_id.as_bytes());
828 hasher.input(quote_id.as_bytes());
829 Self(sha256::Hash::from_engine(hasher).to_byte_array())
830 }
831
832 pub fn from_bytes(bytes: [u8; 32]) -> Self {
834 Self(bytes)
835 }
836
837 pub fn from_hex(value: &str) -> Result<Self, Error> {
839 let bytes = hex::decode(value)?;
840 if bytes.len() != 32 {
841 return Err(Error::InvalidTransactionId);
842 }
843 let mut array = [0u8; 32];
844 array.copy_from_slice(&bytes);
845 Ok(Self(array))
846 }
847
848 pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
850 if slice.len() != 32 {
851 return Err(Error::InvalidTransactionId);
852 }
853 let mut array = [0u8; 32];
854 array.copy_from_slice(slice);
855 Ok(Self(array))
856 }
857
858 pub fn as_bytes(&self) -> &[u8; 32] {
860 &self.0
861 }
862
863 pub fn as_slice(&self) -> &[u8] {
865 &self.0
866 }
867}
868
869impl std::fmt::Display for TransactionId {
870 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871 write!(f, "{}", hex::encode(self.0))
872 }
873}
874
875impl FromStr for TransactionId {
876 type Err = Error;
877
878 fn from_str(value: &str) -> Result<Self, Self::Err> {
879 Self::from_hex(value)
880 }
881}
882
883impl TryFrom<Proofs> for TransactionId {
884 type Error = nut00::Error;
885
886 fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
887 Self::from_proofs(proofs)
888 }
889}
890
891#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
893#[serde(rename_all = "snake_case")]
894pub enum OperationKind {
895 Send,
897 Receive,
899 Swap,
901 Mint,
903 Melt,
905}
906
907impl fmt::Display for OperationKind {
908 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909 match self {
910 OperationKind::Send => write!(f, "send"),
911 OperationKind::Receive => write!(f, "receive"),
912 OperationKind::Swap => write!(f, "swap"),
913 OperationKind::Mint => write!(f, "mint"),
914 OperationKind::Melt => write!(f, "melt"),
915 }
916 }
917}
918
919impl FromStr for OperationKind {
920 type Err = Error;
921
922 fn from_str(s: &str) -> Result<Self, Self::Err> {
923 match s {
924 "send" => Ok(OperationKind::Send),
925 "receive" => Ok(OperationKind::Receive),
926 "swap" => Ok(OperationKind::Swap),
927 "mint" => Ok(OperationKind::Mint),
928 "melt" => Ok(OperationKind::Melt),
929 _ => Err(Error::InvalidOperationKind),
930 }
931 }
932}
933
934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
936pub enum KeysetFilter {
937 Active,
939 All,
941}
942
943#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
948pub enum KeysetLoadPolicy {
949 CacheOnly,
952 #[default]
958 CacheThenNetwork,
959 Refresh,
963}
964
965#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
974#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
975pub trait Wallet: Send + Sync {
976 type Error: std::error::Error + Send + Sync + 'static;
978 type Amount: Clone + Send + Sync;
980 type MintUrl: Clone + Send + Sync;
982 type CurrencyUnit: Clone + Send + Sync;
984 type MintInfo: Clone + Send + Sync;
986 type KeySetInfo: Clone + Send + Sync;
988 type MintQuote: Clone + Send + Sync;
990 type MeltQuote: Clone + Send + Sync;
992 type CrossMintTransferQuote: Clone + Send + Sync;
994 type PaymentMethod: Clone + Send + Sync;
996 type MeltOptions: Clone + Send + Sync;
998 type OperationId: Clone + Send + Sync;
1000 type PreparedSend<'a>: Send + Sync
1002 where
1003 Self: 'a;
1004 type PreparedMelt<'a>: Send + Sync
1006 where
1007 Self: 'a;
1008 type Subscription: Send + Sync;
1010 type SubscribeParams: Clone + Send + Sync;
1012 type RecoveryReport: Clone + Send + Sync;
1014
1015 fn mint_url(&self) -> Self::MintUrl;
1017
1018 fn unit(&self) -> Self::CurrencyUnit;
1020
1021 async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
1023
1024 async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
1026
1027 async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
1029
1030 async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
1032
1033 async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
1035
1036 async fn keysets(&self, policy: KeysetLoadPolicy)
1044 -> Result<Vec<Self::KeySetInfo>, Self::Error>;
1045
1046 async fn active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
1051
1052 async fn keyset(&self, keyset_id: Id) -> Result<Self::KeySetInfo, Self::Error>;
1054
1055 async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Self::Error>;
1057
1058 async fn get_keyset_count_fee(
1060 &self,
1061 keyset_id: &Id,
1062 count: u64,
1063 ) -> Result<Self::Amount, Self::Error>;
1064
1065 async fn get_keyset_fees_and_amounts_by_id(
1067 &self,
1068 keyset_id: Id,
1069 ) -> Result<FeeAndAmounts, Self::Error>;
1070
1071 async fn mint_quote(
1073 &self,
1074 method: Self::PaymentMethod,
1075 amount: Option<Self::Amount>,
1076 description: Option<String>,
1077 extra: Option<String>,
1078 ) -> Result<Self::MintQuote, Self::Error>;
1079
1080 async fn melt_quote(
1082 &self,
1083 method: Self::PaymentMethod,
1084 request: String,
1085 options: Option<Self::MeltOptions>,
1086 extra: Option<String>,
1087 ) -> Result<Self::MeltQuote, Self::Error>;
1088
1089 async fn cross_mint_transfer_quote_max(
1098 &self,
1099 target_wallet: &Self,
1100 ) -> Result<Self::CrossMintTransferQuote, Self::Error>;
1101
1102 async fn list_transactions(
1104 &self,
1105 direction: Option<TransactionDirection>,
1106 ) -> Result<Vec<Transaction>, Self::Error>;
1107
1108 async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
1110
1111 async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
1113
1114 async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
1116
1117 async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
1119
1120 async fn recover_incomplete_sagas(&self) -> Result<Self::RecoveryReport, Self::Error>;
1122
1123 async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
1125
1126 async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
1128
1129 async fn calculate_fee(
1131 &self,
1132 proof_count: u64,
1133 keyset_id: Id,
1134 ) -> Result<Self::Amount, Self::Error>;
1135
1136 async fn receive(
1138 &self,
1139 encoded_token: &str,
1140 options: ReceiveOptions,
1141 ) -> Result<Self::Amount, Self::Error>;
1142
1143 async fn receive_proofs(
1145 &self,
1146 proofs: Proofs,
1147 options: ReceiveOptions,
1148 memo: Option<String>,
1149 token: Option<String>,
1150 ) -> Result<Self::Amount, Self::Error>;
1151
1152 async fn prepare_send(
1154 &self,
1155 amount: Self::Amount,
1156 options: SendOptions,
1157 ) -> Result<Self::PreparedSend<'_>, Self::Error>;
1158
1159 async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
1161
1162 async fn revoke_send(
1164 &self,
1165 operation_id: Self::OperationId,
1166 ) -> Result<Self::Amount, Self::Error>;
1167
1168 async fn check_send_status(&self, operation_id: Self::OperationId)
1170 -> Result<bool, Self::Error>;
1171
1172 async fn mint(
1174 &self,
1175 quote_id: &str,
1176 split_target: SplitTarget,
1177 spending_conditions: Option<SpendingConditions>,
1178 ) -> Result<Proofs, Self::Error>;
1179
1180 async fn mint_unissued_quotes(&self) -> Result<Self::Amount, Self::Error>;
1182
1183 async fn check_mint_quote_status(&self, quote_id: &str)
1185 -> Result<Self::MintQuote, Self::Error>;
1186
1187 async fn fetch_mint_quote(
1189 &self,
1190 quote_id: &str,
1191 payment_method: Option<Self::PaymentMethod>,
1192 ) -> Result<Self::MintQuote, Self::Error>;
1193
1194 async fn prepare_melt(
1196 &self,
1197 quote_id: &str,
1198 metadata: HashMap<String, String>,
1199 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1200
1201 async fn prepare_melt_proofs(
1203 &self,
1204 quote_id: &str,
1205 proofs: Proofs,
1206 metadata: HashMap<String, String>,
1207 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1208
1209 async fn prepare_melt_token(
1215 &self,
1216 quote_id: &str,
1217 encoded_token: &str,
1218 metadata: HashMap<String, String>,
1219 ) -> Result<Self::PreparedMelt<'_>, Self::Error>;
1220
1221 async fn swap(
1223 &self,
1224 amount: Option<Self::Amount>,
1225 split_target: SplitTarget,
1226 input_proofs: Proofs,
1227 spending_conditions: Option<SpendingConditions>,
1228 include_fees: bool,
1229 use_p2bk: bool,
1230 ) -> Result<Option<Proofs>, Self::Error>;
1231
1232 async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
1234
1235 async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
1237
1238 async fn refresh_access_token(&self) -> Result<(), Self::Error>;
1240
1241 async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
1243
1244 async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
1246
1247 async fn restore(&self) -> Result<Restored, Self::Error>;
1249
1250 async fn restore_with_opts(&self, opts: NUT13Options) -> Result<Restored, Self::Error>;
1252
1253 async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
1255
1256 async fn subscribe_mint_quote_state(
1261 &self,
1262 quote_ids: Vec<String>,
1263 method: Self::PaymentMethod,
1264 ) -> Result<Self::Subscription, Self::Error>;
1265
1266 fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
1272
1273 #[cfg(feature = "http")]
1279 fn set_rate_limiting_config(&self, config: Option<RateLimitConfig>);
1280
1281 #[cfg(feature = "http")]
1286 fn is_rate_limited(&self) -> bool;
1287
1288 #[cfg(feature = "http")]
1294 async fn flush_rate_limits(&self);
1295
1296 async fn subscribe(
1298 &self,
1299 params: Self::SubscribeParams,
1300 ) -> Result<Self::Subscription, Self::Error>;
1301
1302 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1304 async fn melt_bip353_quote(
1305 &self,
1306 bip353_address: &str,
1307 amount_msat: Self::Amount,
1308 network: bitcoin::Network,
1309 ) -> Result<Self::MeltQuote, Self::Error>;
1310
1311 #[cfg(not(target_arch = "wasm32"))]
1313 async fn melt_lightning_address_quote(
1314 &self,
1315 lightning_address: &str,
1316 amount_msat: Self::Amount,
1317 ) -> Result<Self::MeltQuote, Self::Error>;
1318
1319 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1325 async fn melt_human_readable_quote(
1326 &self,
1327 address: &str,
1328 amount_msat: Self::Amount,
1329 network: bitcoin::Network,
1330 ) -> Result<Self::MeltQuote, Self::Error>;
1331
1332 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
1334 async fn melt_human_readable(
1335 &self,
1336 address: &str,
1337 amount_msat: Self::Amount,
1338 network: bitcoin::Network,
1339 ) -> Result<Self::MeltQuote, Self::Error> {
1340 self.melt_human_readable_quote(address, amount_msat, network)
1341 .await
1342 }
1343
1344 async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
1346 self.check_mint_quote_status(quote_id).await
1347 }
1348
1349 async fn mint_unified(
1351 &self,
1352 quote_id: &str,
1353 split_target: SplitTarget,
1354 spending_conditions: Option<SpendingConditions>,
1355 ) -> Result<Proofs, Self::Error> {
1356 self.mint(quote_id, split_target, spending_conditions).await
1357 }
1358
1359 async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
1365
1366 async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
1369
1370 async fn get_public_key(
1372 &self,
1373 pubkey: &PublicKey,
1374 ) -> Result<Option<P2PKSigningKey>, Self::Error>;
1375
1376 async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
1378
1379 async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
1381
1382 async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
1384}
1385
1386#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1388pub struct P2PKSigningKey {
1389 pub pubkey: PublicKey,
1391 pub derivation_path: DerivationPath,
1393 pub derivation_index: u32,
1395 pub created_time: u64,
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401 use super::*;
1402 use crate::nuts::Id;
1403 use crate::secret::Secret;
1404
1405 #[test]
1406 fn test_transaction_id_from_hex() {
1407 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
1408 let transaction_id = TransactionId::from_hex(hex_str).unwrap();
1409 assert_eq!(transaction_id.to_string(), hex_str);
1410 }
1411
1412 #[test]
1413 fn test_transaction_id_from_hex_empty_string() {
1414 let hex_str = "";
1415 let res = TransactionId::from_hex(hex_str);
1416 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1417 }
1418
1419 #[test]
1420 fn test_transaction_id_from_hex_longer_string() {
1421 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
1422 let res = TransactionId::from_hex(hex_str);
1423 assert!(matches!(res, Err(Error::InvalidTransactionId)));
1424 }
1425
1426 #[test]
1427 fn transaction_id_from_saga_id_uses_canonical_uuid_bytes() {
1428 let saga_id =
1429 Uuid::parse_str("019fa338-b72f-7f21-9bb2-a504cdd5927b").expect("valid saga ID");
1430 let transaction_id = TransactionId::from_saga_id(saga_id);
1431
1432 assert_eq!(
1433 transaction_id.as_bytes(),
1434 b"019fa338b72f7f219bb2a504cdd5927b"
1435 );
1436 }
1437
1438 #[test]
1439 fn batch_quote_transaction_ids_are_stable_and_distinct() {
1440 let saga_id =
1441 Uuid::parse_str("019fa338-b72f-7f21-9bb2-a504cdd5927b").expect("valid saga ID");
1442
1443 let first = TransactionId::from_batch_quote(saga_id, "quote-a");
1444 let first_again = TransactionId::from_batch_quote(saga_id, "quote-a");
1445 let second = TransactionId::from_batch_quote(saga_id, "quote-b");
1446
1447 assert_eq!(first, first_again);
1448 assert_ne!(first, second);
1449 assert_ne!(first, TransactionId::from_saga_id(saga_id));
1450 }
1451
1452 #[test]
1453 fn saga_managed_transactions_with_the_same_ys_have_distinct_ids() {
1454 let ys = vec![SecretKey::generate().public_key()];
1455 let transaction = Transaction {
1456 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1457 direction: TransactionDirection::Outgoing,
1458 amount: Amount::from(10),
1459 fee: Amount::ZERO,
1460 unit: CurrencyUnit::Sat,
1461 ys,
1462 timestamp: 42,
1463 memo: None,
1464 metadata: HashMap::new(),
1465 quote_id: None,
1466 payment_request: None,
1467 payment_proof: None,
1468 payment_method: None,
1469 saga_id: Some(Uuid::new_v4()),
1470 status: TransactionStatus::Pending,
1471 };
1472 let mut received = transaction.clone();
1473 received.direction = TransactionDirection::Incoming;
1474 received.saga_id = Some(Uuid::new_v4());
1475
1476 assert_ne!(transaction.id(), received.id());
1477 }
1478
1479 #[test]
1480 fn test_matches_conditions() {
1481 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1482 let proof = Proof::new(
1483 Amount::from(64),
1484 keyset_id,
1485 Secret::new("test_secret"),
1486 PublicKey::from_hex(
1487 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1488 )
1489 .unwrap(),
1490 );
1491
1492 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1493 let proof_info =
1494 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1495
1496 assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
1498 assert!(!proof_info.matches_conditions(
1499 &Some(MintUrl::from_str("https://different.com").unwrap()),
1500 &None,
1501 &None,
1502 &None
1503 ));
1504
1505 assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
1507 assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
1508
1509 assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
1511 assert!(proof_info.matches_conditions(
1512 &None,
1513 &None,
1514 &Some(vec![State::Unspent, State::Spent]),
1515 &None
1516 ));
1517 assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
1518
1519 assert!(proof_info.matches_conditions(&None, &None, &None, &None));
1521
1522 assert!(proof_info.matches_conditions(
1524 &Some(mint_url),
1525 &Some(CurrencyUnit::Sat),
1526 &Some(vec![State::Unspent]),
1527 &None
1528 ));
1529 }
1530
1531 #[test]
1532 fn test_matches_conditions_with_spending_conditions() {
1533 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1538 let proof = Proof::new(
1539 Amount::from(64),
1540 keyset_id,
1541 Secret::new("test_secret"),
1542 PublicKey::from_hex(
1543 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1544 )
1545 .unwrap(),
1546 );
1547
1548 let mint_url = MintUrl::from_str("https://example.com").unwrap();
1549 let proof_info =
1550 ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
1551
1552 assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
1554
1555 let dummy_condition = SpendingConditions::P2PKConditions {
1557 data: SecretKey::generate().public_key(),
1558 conditions: None,
1559 };
1560 assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
1561 }
1562
1563 #[test]
1564 fn wallet_record_debug_redacts_spendable_secrets() {
1565 let proof_secret = "wallet-proof-secret";
1566 let payment_proof = "wallet-payment-preimage";
1567 let keyset_id = Id::from_str("00deadbeef123456").expect("valid keyset ID");
1568 let proof = Proof::new(
1569 Amount::from(64),
1570 keyset_id,
1571 Secret::new(proof_secret),
1572 PublicKey::from_hex(
1573 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1574 )
1575 .expect("valid public key"),
1576 );
1577 let mint_url = MintUrl::from_str("https://mint.example.com").expect("valid mint URL");
1578 let proof_info = ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat)
1579 .expect("valid proof");
1580 let melt_quote = MeltQuote {
1581 id: "public-quote-id".to_string(),
1582 mint_url: Some(mint_url.clone()),
1583 unit: CurrencyUnit::Sat,
1584 amount: Amount::from(10),
1585 request: "public-payment-request".to_string(),
1586 fee_reserve: Amount::from(1),
1587 state: MeltQuoteState::Paid,
1588 expiry: 1_000,
1589 payment_proof: Some(payment_proof.to_string()),
1590 estimated_blocks: None,
1591 fee_index: None,
1592 payment_method: PaymentMethod::BOLT11,
1593 used_by_operation: None,
1594 version: 0,
1595 };
1596 let transaction = Transaction {
1597 mint_url,
1598 direction: TransactionDirection::Outgoing,
1599 amount: Amount::from(10),
1600 fee: Amount::from(1),
1601 unit: CurrencyUnit::Sat,
1602 ys: vec![],
1603 timestamp: 42,
1604 memo: None,
1605 metadata: HashMap::new(),
1606 quote_id: Some("public-quote-id".to_string()),
1607 payment_request: None,
1608 payment_proof: Some(payment_proof.to_string()),
1609 payment_method: Some(PaymentMethod::BOLT11),
1610 saga_id: None,
1611 status: TransactionStatus::Completed,
1612 };
1613
1614 for debug in [
1615 format!("{proof_info:?}"),
1616 format!("{melt_quote:?}"),
1617 format!("{transaction:?}"),
1618 ] {
1619 assert!(debug.contains("[REDACTED]"));
1620 assert!(!debug.contains(proof_secret));
1621 assert!(!debug.contains(payment_proof));
1622 }
1623 }
1624
1625 #[test]
1626 fn test_wallet_options_debug_redacts_p2pk_signing_keys() {
1627 let secret_key = SecretKey::generate();
1628 let secret_hex = secret_key.to_secret_hex();
1629 let preimage = "super_secret_htlc_preimage_xyz";
1630
1631 let send_options = SendOptions {
1632 p2pk_signing_keys: vec![secret_key.clone()],
1633 ..Default::default()
1634 };
1635 let receive_options = ReceiveOptions {
1636 p2pk_signing_keys: vec![secret_key],
1637 preimages: vec![preimage.to_string()],
1638 ..Default::default()
1639 };
1640
1641 let send_debug = format!("{:?}", send_options);
1642 let receive_debug = format!("{:?}", receive_options);
1643
1644 assert!(!send_debug.contains(&secret_hex));
1645 assert!(send_debug.contains("[redacted]"));
1646 assert!(!receive_debug.contains(&secret_hex));
1647 assert!(!receive_debug.contains(preimage));
1648 assert!(receive_debug.contains("[redacted]"));
1649 }
1650
1651 #[test]
1652 fn nut13_options_defaults_match_nut13_spec() {
1653 let opts = NUT13Options::default();
1656 assert_eq!(opts.batch_size, NUT13Options::DEFAULT_BATCH_SIZE);
1657 assert_eq!(opts.max_gap, NUT13Options::DEFAULT_MAX_GAP);
1658 }
1659
1660 #[test]
1661 fn nut13_options_new_accepts_custom_values() {
1662 let opts = NUT13Options::new(25, 2).unwrap();
1663 let cloned = opts.clone();
1664 assert_eq!(cloned.batch_size, 25);
1665 assert_eq!(cloned.max_gap, 2);
1666 }
1667
1668 #[test]
1669 fn nut13_options_reject_zero_batch_size() {
1670 let err = NUT13Options::new(0, 2).unwrap_err();
1671 assert!(matches!(
1672 err,
1673 Error::InvalidNut13Options {
1674 field: "batch_size",
1675 ..
1676 }
1677 ));
1678 }
1679
1680 #[test]
1681 fn nut13_options_reject_zero_max_gap() {
1682 let err = NUT13Options::new(25, 0).unwrap_err();
1683 assert!(matches!(
1684 err,
1685 Error::InvalidNut13Options {
1686 field: "max_gap",
1687 ..
1688 }
1689 ));
1690 }
1691
1692 #[test]
1693 fn transaction_status_round_trips_and_rejects_unknown_values() {
1694 for status in [
1695 TransactionStatus::Pending,
1696 TransactionStatus::Completed,
1697 TransactionStatus::Failed,
1698 ] {
1699 assert_eq!(
1700 TransactionStatus::from_str(&status.to_string()).expect("valid status"),
1701 status
1702 );
1703 }
1704
1705 assert!(matches!(
1706 TransactionStatus::from_str("unknown"),
1707 Err(Error::InvalidTransactionStatus)
1708 ));
1709 }
1710
1711 #[test]
1712 fn transaction_without_status_defaults_to_completed() {
1713 let transaction = Transaction {
1714 mint_url: MintUrl::from_str("https://mint.example.com").expect("valid mint URL"),
1715 direction: TransactionDirection::Incoming,
1716 amount: Amount::from(10),
1717 fee: Amount::ZERO,
1718 unit: CurrencyUnit::Sat,
1719 ys: vec![SecretKey::generate().public_key()],
1720 timestamp: 42,
1721 memo: None,
1722 metadata: HashMap::new(),
1723 quote_id: None,
1724 payment_request: None,
1725 payment_proof: None,
1726 payment_method: None,
1727 saga_id: None,
1728 status: TransactionStatus::Pending,
1729 };
1730 let mut value = serde_json::to_value(transaction).expect("serialize transaction");
1731 value
1732 .as_object_mut()
1733 .expect("transaction serializes as an object")
1734 .remove("status");
1735
1736 let decoded: Transaction =
1737 serde_json::from_value(value).expect("deserialize legacy transaction");
1738
1739 assert_eq!(decoded.status, TransactionStatus::Completed);
1740 }
1741}