1use std::collections::HashMap;
4use std::fmt;
5use std::str::FromStr;
6
7use bitcoin::hashes::{sha256, Hash, HashEngine};
8use cashu::util::hex;
9use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use crate::mint_url::MintUrl;
14use crate::nuts::{
15 CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey, SpendingConditions, State,
16};
17use crate::{Amount, Error};
18
19pub mod saga;
20
21pub use saga::{
22 IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
23 ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
24 SwapSagaState, WalletSaga, WalletSagaState,
25};
26
27#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29pub struct WalletKey {
30 pub mint_url: MintUrl,
32 pub unit: CurrencyUnit,
34}
35
36impl fmt::Display for WalletKey {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
39 }
40}
41
42impl WalletKey {
43 pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
45 Self { mint_url, unit }
46 }
47}
48
49#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
51pub struct ProofInfo {
52 pub proof: Proof,
54 pub y: PublicKey,
56 pub mint_url: MintUrl,
58 pub state: State,
60 pub spending_condition: Option<SpendingConditions>,
62 pub unit: CurrencyUnit,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub used_by_operation: Option<Uuid>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub created_by_operation: Option<Uuid>,
70}
71
72impl ProofInfo {
73 pub fn new(
75 proof: Proof,
76 mint_url: MintUrl,
77 state: State,
78 unit: CurrencyUnit,
79 ) -> Result<Self, Error> {
80 let y = proof.y()?;
81
82 let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
83
84 Ok(Self {
85 proof,
86 y,
87 mint_url,
88 state,
89 spending_condition,
90 unit,
91 used_by_operation: None,
92 created_by_operation: None,
93 })
94 }
95
96 pub fn new_with_operations(
98 proof: Proof,
99 mint_url: MintUrl,
100 state: State,
101 unit: CurrencyUnit,
102 used_by_operation: Option<Uuid>,
103 created_by_operation: Option<Uuid>,
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,
117 created_by_operation,
118 })
119 }
120
121 pub fn matches_conditions(
123 &self,
124 mint_url: &Option<MintUrl>,
125 unit: &Option<CurrencyUnit>,
126 state: &Option<Vec<State>>,
127 spending_conditions: &Option<Vec<SpendingConditions>>,
128 ) -> bool {
129 if let Some(mint_url) = mint_url {
130 if mint_url.ne(&self.mint_url) {
131 return false;
132 }
133 }
134
135 if let Some(unit) = unit {
136 if unit.ne(&self.unit) {
137 return false;
138 }
139 }
140
141 if let Some(state) = state {
142 if !state.contains(&self.state) {
143 return false;
144 }
145 }
146
147 if let Some(spending_conditions) = spending_conditions {
148 match &self.spending_condition {
149 None => {
150 if !spending_conditions.is_empty() {
151 return false;
152 }
153 }
154 Some(s) => {
155 if !spending_conditions.contains(s) {
156 return false;
157 }
158 }
159 }
160 }
161
162 true
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct MintQuote {
169 pub id: String,
171 pub mint_url: MintUrl,
173 pub payment_method: PaymentMethod,
175 pub amount: Option<Amount>,
177 pub unit: CurrencyUnit,
179 pub request: String,
181 pub state: MintQuoteState,
183 pub expiry: u64,
185 pub secret_key: Option<SecretKey>,
187 #[serde(default)]
189 pub amount_issued: Amount,
190 #[serde(default)]
192 pub amount_paid: Amount,
193 #[serde(default)]
195 pub used_by_operation: Option<String>,
196 #[serde(default)]
198 pub version: u32,
199}
200
201#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
203pub struct MeltQuote {
204 pub id: String,
206 pub unit: CurrencyUnit,
208 pub amount: Amount,
210 pub request: String,
212 pub fee_reserve: Amount,
214 pub state: MeltQuoteState,
216 pub expiry: u64,
218 pub payment_preimage: Option<String>,
220 pub payment_method: PaymentMethod,
222 #[serde(default)]
224 pub used_by_operation: Option<String>,
225 #[serde(default)]
227 pub version: u32,
228}
229
230impl MintQuote {
231 #[allow(clippy::too_many_arguments)]
233 pub fn new(
234 id: String,
235 mint_url: MintUrl,
236 payment_method: PaymentMethod,
237 amount: Option<Amount>,
238 unit: CurrencyUnit,
239 request: String,
240 expiry: u64,
241 secret_key: Option<SecretKey>,
242 ) -> Self {
243 Self {
244 id,
245 mint_url,
246 payment_method,
247 amount,
248 unit,
249 request,
250 state: MintQuoteState::Unpaid,
251 expiry,
252 secret_key,
253 amount_issued: Amount::ZERO,
254 amount_paid: Amount::ZERO,
255 used_by_operation: None,
256 version: 0,
257 }
258 }
259
260 pub fn total_amount(&self) -> Amount {
262 self.amount_paid
263 }
264
265 pub fn is_expired(&self, current_time: u64) -> bool {
267 current_time > self.expiry
268 }
269
270 pub fn amount_mintable(&self) -> Amount {
272 if self.payment_method == PaymentMethod::BOLT11 {
273 if self.state == MintQuoteState::Paid {
275 self.amount.unwrap_or(Amount::ZERO)
276 } else {
277 Amount::ZERO
278 }
279 } else {
280 self.amount_paid
282 .checked_sub(self.amount_issued)
283 .unwrap_or(Amount::ZERO)
284 }
285 }
286}
287
288#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
290pub enum SendKind {
291 #[default]
292 OnlineExact,
294 OnlineTolerance(Amount),
296 OfflineExact,
298 OfflineTolerance(Amount),
300}
301
302impl SendKind {
303 pub fn is_online(&self) -> bool {
305 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
306 }
307
308 pub fn is_offline(&self) -> bool {
310 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
311 }
312
313 pub fn is_exact(&self) -> bool {
315 matches!(self, Self::OnlineExact | Self::OfflineExact)
316 }
317
318 pub fn has_tolerance(&self) -> bool {
320 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326pub struct Transaction {
327 pub mint_url: MintUrl,
329 pub direction: TransactionDirection,
331 pub amount: Amount,
333 pub fee: Amount,
335 pub unit: CurrencyUnit,
337 pub ys: Vec<PublicKey>,
339 pub timestamp: u64,
341 pub memo: Option<String>,
343 pub metadata: HashMap<String, String>,
345 pub quote_id: Option<String>,
347 pub payment_request: Option<String>,
349 pub payment_proof: Option<String>,
351 #[serde(default)]
353 pub payment_method: Option<PaymentMethod>,
354 #[serde(default)]
356 pub saga_id: Option<Uuid>,
357}
358
359impl Transaction {
360 pub fn id(&self) -> TransactionId {
362 TransactionId::new(self.ys.clone())
363 }
364
365 pub fn matches_conditions(
367 &self,
368 mint_url: &Option<MintUrl>,
369 direction: &Option<TransactionDirection>,
370 unit: &Option<CurrencyUnit>,
371 ) -> bool {
372 if let Some(mint_url) = mint_url {
373 if &self.mint_url != mint_url {
374 return false;
375 }
376 }
377 if let Some(direction) = direction {
378 if &self.direction != direction {
379 return false;
380 }
381 }
382 if let Some(unit) = unit {
383 if &self.unit != unit {
384 return false;
385 }
386 }
387 true
388 }
389}
390
391impl PartialOrd for Transaction {
392 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
393 Some(self.cmp(other))
394 }
395}
396
397impl Ord for Transaction {
398 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
399 self.timestamp
400 .cmp(&other.timestamp)
401 .reverse()
402 .then_with(|| self.id().cmp(&other.id()))
403 }
404}
405
406#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub enum TransactionDirection {
409 Incoming,
411 Outgoing,
413}
414
415impl std::fmt::Display for TransactionDirection {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 match self {
418 TransactionDirection::Incoming => write!(f, "Incoming"),
419 TransactionDirection::Outgoing => write!(f, "Outgoing"),
420 }
421 }
422}
423
424impl FromStr for TransactionDirection {
425 type Err = Error;
426
427 fn from_str(value: &str) -> Result<Self, Self::Err> {
428 match value {
429 "Incoming" => Ok(Self::Incoming),
430 "Outgoing" => Ok(Self::Outgoing),
431 _ => Err(Error::InvalidTransactionDirection),
432 }
433 }
434}
435
436#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
438#[serde(transparent)]
439pub struct TransactionId([u8; 32]);
440
441impl TransactionId {
442 pub fn new(ys: Vec<PublicKey>) -> Self {
444 let mut ys = ys;
445 ys.sort();
446 let mut hasher = sha256::Hash::engine();
447 for y in ys {
448 hasher.input(&y.to_bytes());
449 }
450 let hash = sha256::Hash::from_engine(hasher);
451 Self(hash.to_byte_array())
452 }
453
454 pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
456 let ys = proofs
457 .iter()
458 .map(|proof| proof.y())
459 .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
460 Ok(Self::new(ys))
461 }
462
463 pub fn from_bytes(bytes: [u8; 32]) -> Self {
465 Self(bytes)
466 }
467
468 pub fn from_hex(value: &str) -> Result<Self, Error> {
470 let bytes = hex::decode(value)?;
471 if bytes.len() != 32 {
472 return Err(Error::InvalidTransactionId);
473 }
474 let mut array = [0u8; 32];
475 array.copy_from_slice(&bytes);
476 Ok(Self(array))
477 }
478
479 pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
481 if slice.len() != 32 {
482 return Err(Error::InvalidTransactionId);
483 }
484 let mut array = [0u8; 32];
485 array.copy_from_slice(slice);
486 Ok(Self(array))
487 }
488
489 pub fn as_bytes(&self) -> &[u8; 32] {
491 &self.0
492 }
493
494 pub fn as_slice(&self) -> &[u8] {
496 &self.0
497 }
498}
499
500impl std::fmt::Display for TransactionId {
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 write!(f, "{}", hex::encode(self.0))
503 }
504}
505
506impl FromStr for TransactionId {
507 type Err = Error;
508
509 fn from_str(value: &str) -> Result<Self, Self::Err> {
510 Self::from_hex(value)
511 }
512}
513
514impl TryFrom<Proofs> for TransactionId {
515 type Error = nut00::Error;
516
517 fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
518 Self::from_proofs(proofs)
519 }
520}
521
522#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum OperationKind {
526 Send,
528 Receive,
530 Swap,
532 Mint,
534 Melt,
536}
537
538impl fmt::Display for OperationKind {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 match self {
541 OperationKind::Send => write!(f, "send"),
542 OperationKind::Receive => write!(f, "receive"),
543 OperationKind::Swap => write!(f, "swap"),
544 OperationKind::Mint => write!(f, "mint"),
545 OperationKind::Melt => write!(f, "melt"),
546 }
547 }
548}
549
550impl FromStr for OperationKind {
551 type Err = Error;
552
553 fn from_str(s: &str) -> Result<Self, Self::Err> {
554 match s {
555 "send" => Ok(OperationKind::Send),
556 "receive" => Ok(OperationKind::Receive),
557 "swap" => Ok(OperationKind::Swap),
558 "mint" => Ok(OperationKind::Mint),
559 "melt" => Ok(OperationKind::Melt),
560 _ => Err(Error::InvalidOperationKind),
561 }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::nuts::Id;
569 use crate::secret::Secret;
570
571 #[test]
572 fn test_transaction_id_from_hex() {
573 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
574 let transaction_id = TransactionId::from_hex(hex_str).unwrap();
575 assert_eq!(transaction_id.to_string(), hex_str);
576 }
577
578 #[test]
579 fn test_transaction_id_from_hex_empty_string() {
580 let hex_str = "";
581 let res = TransactionId::from_hex(hex_str);
582 assert!(matches!(res, Err(Error::InvalidTransactionId)));
583 }
584
585 #[test]
586 fn test_transaction_id_from_hex_longer_string() {
587 let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
588 let res = TransactionId::from_hex(hex_str);
589 assert!(matches!(res, Err(Error::InvalidTransactionId)));
590 }
591
592 #[test]
593 fn test_matches_conditions() {
594 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
595 let proof = Proof::new(
596 Amount::from(64),
597 keyset_id,
598 Secret::new("test_secret"),
599 PublicKey::from_hex(
600 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
601 )
602 .unwrap(),
603 );
604
605 let mint_url = MintUrl::from_str("https://example.com").unwrap();
606 let proof_info =
607 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
608
609 assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
611 assert!(!proof_info.matches_conditions(
612 &Some(MintUrl::from_str("https://different.com").unwrap()),
613 &None,
614 &None,
615 &None
616 ));
617
618 assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
620 assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
621
622 assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
624 assert!(proof_info.matches_conditions(
625 &None,
626 &None,
627 &Some(vec![State::Unspent, State::Spent]),
628 &None
629 ));
630 assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
631
632 assert!(proof_info.matches_conditions(&None, &None, &None, &None));
634
635 assert!(proof_info.matches_conditions(
637 &Some(mint_url),
638 &Some(CurrencyUnit::Sat),
639 &Some(vec![State::Unspent]),
640 &None
641 ));
642 }
643
644 #[test]
645 fn test_matches_conditions_with_spending_conditions() {
646 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
651 let proof = Proof::new(
652 Amount::from(64),
653 keyset_id,
654 Secret::new("test_secret"),
655 PublicKey::from_hex(
656 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
657 )
658 .unwrap(),
659 );
660
661 let mint_url = MintUrl::from_str("https://example.com").unwrap();
662 let proof_info =
663 ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
664
665 assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
667
668 let dummy_condition = SpendingConditions::P2PKConditions {
670 data: SecretKey::generate().public_key(),
671 conditions: None,
672 };
673 assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
674 }
675}