Skip to main content

cdk_common/wallet/saga/
issue.rs

1//! Issue (mint) saga types
2
3use cashu::BlindedMessage;
4use serde::{Deserialize, Serialize};
5
6use crate::Error;
7
8/// States specific to mint (issue) saga
9#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum IssueSagaState {
12    /// Pre-mint secrets created and counter incremented, ready to request signatures
13    SecretsPrepared,
14    /// Mint request sent to mint, awaiting signatures for new proofs
15    MintRequested,
16}
17
18impl std::fmt::Display for IssueSagaState {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            IssueSagaState::SecretsPrepared => write!(f, "secrets_prepared"),
22            IssueSagaState::MintRequested => write!(f, "mint_requested"),
23        }
24    }
25}
26
27impl std::str::FromStr for IssueSagaState {
28    type Err = Error;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s {
31            "secrets_prepared" => Ok(IssueSagaState::SecretsPrepared),
32            "mint_requested" => Ok(IssueSagaState::MintRequested),
33            _ => Err(Error::InvalidOperationState),
34        }
35    }
36}
37
38/// Operation-specific data for Mint operations
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct MintOperationData {
41    /// Quote ID (for single mint, or first quote in batch)
42    quote_id: String,
43    /// Quote IDs for batch operations
44    ///
45    /// If present, this is a batch operation. The batch may have one or more quotes.
46    /// For backward compatibility with existing sagas, check this field first.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    quote_ids: Option<Vec<String>>,
49    /// Whether this is a batch operation
50    ///
51    /// True if this was created by batch_mint, false for single mint.
52    /// Used to determine which endpoint to use for replay.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub is_batch: Option<bool>,
55    /// Amount to mint (total for batch)
56    pub amount: crate::Amount,
57    /// Derivation counter start
58    pub counter_start: Option<u32>,
59    /// Derivation counter end
60    pub counter_end: Option<u32>,
61    /// Blinded messages for recovery
62    ///
63    /// Stored so that if a crash occurs after the mint accepts the request,
64    /// we can use these to query the mint for signatures and reconstruct proofs.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub blinded_messages: Option<Vec<BlindedMessage>>,
67    /// Number of consecutive blinded messages assigned to each batch quote.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub batch_output_counts: Option<Vec<usize>>,
70    /// Amount issued for each batch quote, in quote order.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub batch_quote_amounts: Option<Vec<crate::Amount>>,
73}
74
75impl MintOperationData {
76    /// Create operation data for a single-quote mint operation.
77    pub fn new_single(
78        quote_id: String,
79        amount: crate::Amount,
80        counter_start: Option<u32>,
81        counter_end: Option<u32>,
82        blinded_messages: Option<Vec<BlindedMessage>>,
83    ) -> Self {
84        Self {
85            quote_ids: Some(vec![quote_id.clone()]),
86            quote_id,
87            is_batch: Some(false),
88            amount,
89            counter_start,
90            counter_end,
91            blinded_messages,
92            batch_output_counts: None,
93            batch_quote_amounts: None,
94        }
95    }
96
97    /// Create operation data for a batch mint operation.
98    pub fn new_batch(
99        quote_ids: Vec<String>,
100        amount: crate::Amount,
101        counter_start: Option<u32>,
102        counter_end: Option<u32>,
103        blinded_messages: Option<Vec<BlindedMessage>>,
104    ) -> Self {
105        let quote_id = quote_ids.first().cloned().unwrap_or_default();
106
107        Self {
108            quote_id,
109            quote_ids: Some(quote_ids),
110            is_batch: Some(true),
111            amount,
112            counter_start,
113            counter_end,
114            blinded_messages,
115            batch_output_counts: None,
116            batch_quote_amounts: None,
117        }
118    }
119
120    /// Create operation data for a partitioned batch mint operation.
121    pub fn new_partitioned_batch(
122        quote_ids: Vec<String>,
123        amount: crate::Amount,
124        counter_start: Option<u32>,
125        counter_end: Option<u32>,
126        blinded_messages: Option<Vec<BlindedMessage>>,
127        batch_output_counts: Vec<usize>,
128        batch_quote_amounts: Vec<crate::Amount>,
129    ) -> Self {
130        let quote_id = quote_ids.first().cloned().unwrap_or_default();
131
132        Self {
133            quote_id,
134            quote_ids: Some(quote_ids),
135            is_batch: Some(true),
136            amount,
137            counter_start,
138            counter_end,
139            blinded_messages,
140            batch_output_counts: Some(batch_output_counts),
141            batch_quote_amounts: Some(batch_quote_amounts),
142        }
143    }
144
145    /// Get the representative quote ID for this operation.
146    pub fn primary_quote_id(&self) -> &str {
147        &self.quote_id
148    }
149
150    /// Get all quote IDs for this operation.
151    ///
152    /// Returns quote_ids if this is a batch, otherwise wraps quote_id in a vec.
153    pub fn quote_ids(&self) -> Vec<String> {
154        if let Some(ref ids) = self.quote_ids {
155            ids.clone()
156        } else {
157            vec![self.quote_id.clone()]
158        }
159    }
160
161    /// Check if this is a batch operation.
162    pub fn is_batch(&self) -> bool {
163        self.is_batch.unwrap_or(false)
164    }
165}