cdk_common/
wallet.rs

1//! Wallet Types
2
3use 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, Proofs, PublicKey};
10use serde::{Deserialize, Serialize};
11
12use crate::mint_url::MintUrl;
13use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
14use crate::{Amount, Error};
15
16/// Wallet Key
17#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18pub struct WalletKey {
19    /// Mint Url
20    pub mint_url: MintUrl,
21    /// Currency Unit
22    pub unit: CurrencyUnit,
23}
24
25impl fmt::Display for WalletKey {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
28    }
29}
30
31impl WalletKey {
32    /// Create new [`WalletKey`]
33    pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
34        Self { mint_url, unit }
35    }
36}
37
38/// Mint Quote Info
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct MintQuote {
41    /// Quote id
42    pub id: String,
43    /// Mint Url
44    pub mint_url: MintUrl,
45    /// Payment method
46    #[serde(default)]
47    pub payment_method: PaymentMethod,
48    /// Amount of quote
49    pub amount: Option<Amount>,
50    /// Unit of quote
51    pub unit: CurrencyUnit,
52    /// Quote payment request e.g. bolt11
53    pub request: String,
54    /// Quote state
55    pub state: MintQuoteState,
56    /// Expiration time of quote
57    pub expiry: u64,
58    /// Secretkey for signing mint quotes [NUT-20]
59    pub secret_key: Option<SecretKey>,
60    /// Amount minted
61    #[serde(default)]
62    pub amount_issued: Amount,
63    /// Amount paid to the mint for the quote
64    #[serde(default)]
65    pub amount_paid: Amount,
66}
67
68/// Melt Quote Info
69#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
70pub struct MeltQuote {
71    /// Quote id
72    pub id: String,
73    /// Quote unit
74    pub unit: CurrencyUnit,
75    /// Quote amount
76    pub amount: Amount,
77    /// Quote Payment request e.g. bolt11
78    pub request: String,
79    /// Quote fee reserve
80    pub fee_reserve: Amount,
81    /// Quote state
82    pub state: MeltQuoteState,
83    /// Expiration time of quote
84    pub expiry: u64,
85    /// Payment preimage
86    pub payment_preimage: Option<String>,
87    /// Payment method
88    #[serde(default)]
89    pub payment_method: PaymentMethod,
90}
91
92impl MintQuote {
93    /// Create a new MintQuote
94    #[allow(clippy::too_many_arguments)]
95    pub fn new(
96        id: String,
97        mint_url: MintUrl,
98        payment_method: PaymentMethod,
99        amount: Option<Amount>,
100        unit: CurrencyUnit,
101        request: String,
102        expiry: u64,
103        secret_key: Option<SecretKey>,
104    ) -> Self {
105        Self {
106            id,
107            mint_url,
108            payment_method,
109            amount,
110            unit,
111            request,
112            state: MintQuoteState::Unpaid,
113            expiry,
114            secret_key,
115            amount_issued: Amount::ZERO,
116            amount_paid: Amount::ZERO,
117        }
118    }
119
120    /// Calculate the total amount including any fees
121    pub fn total_amount(&self) -> Amount {
122        self.amount_paid
123    }
124
125    /// Check if the quote has expired
126    pub fn is_expired(&self, current_time: u64) -> bool {
127        current_time > self.expiry
128    }
129
130    /// Amount that can be minted
131    pub fn amount_mintable(&self) -> Amount {
132        if self.amount_issued > self.amount_paid {
133            return Amount::ZERO;
134        }
135
136        let difference = self.amount_paid - self.amount_issued;
137
138        if difference == Amount::ZERO && self.state != MintQuoteState::Issued {
139            if let Some(amount) = self.amount {
140                return amount;
141            }
142        }
143
144        difference
145    }
146}
147
148/// Send Kind
149#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
150pub enum SendKind {
151    #[default]
152    /// Allow online swap before send if wallet does not have exact amount
153    OnlineExact,
154    /// Prefer offline send if difference is less then tolerance
155    OnlineTolerance(Amount),
156    /// Wallet cannot do an online swap and selected proof must be exactly send amount
157    OfflineExact,
158    /// Wallet must remain offline but can over pay if below tolerance
159    OfflineTolerance(Amount),
160}
161
162impl SendKind {
163    /// Check if send kind is online
164    pub fn is_online(&self) -> bool {
165        matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
166    }
167
168    /// Check if send kind is offline
169    pub fn is_offline(&self) -> bool {
170        matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
171    }
172
173    /// Check if send kind is exact
174    pub fn is_exact(&self) -> bool {
175        matches!(self, Self::OnlineExact | Self::OfflineExact)
176    }
177
178    /// Check if send kind has tolerance
179    pub fn has_tolerance(&self) -> bool {
180        matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
181    }
182}
183
184/// Wallet Transaction
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct Transaction {
187    /// Mint Url
188    pub mint_url: MintUrl,
189    /// Transaction direction
190    pub direction: TransactionDirection,
191    /// Amount
192    pub amount: Amount,
193    /// Fee
194    pub fee: Amount,
195    /// Currency Unit
196    pub unit: CurrencyUnit,
197    /// Proof Ys
198    pub ys: Vec<PublicKey>,
199    /// Unix timestamp
200    pub timestamp: u64,
201    /// Memo
202    pub memo: Option<String>,
203    /// User-defined metadata
204    pub metadata: HashMap<String, String>,
205    /// Quote ID if this is a mint or melt transaction
206    pub quote_id: Option<String>,
207    /// Payment request (e.g., BOLT11 invoice, BOLT12 offer)
208    pub payment_request: Option<String>,
209    /// Payment proof (e.g., preimage for Lightning melt transactions)
210    pub payment_proof: Option<String>,
211}
212
213impl Transaction {
214    /// Transaction ID
215    pub fn id(&self) -> TransactionId {
216        TransactionId::new(self.ys.clone())
217    }
218
219    /// Check if transaction matches conditions
220    pub fn matches_conditions(
221        &self,
222        mint_url: &Option<MintUrl>,
223        direction: &Option<TransactionDirection>,
224        unit: &Option<CurrencyUnit>,
225    ) -> bool {
226        if let Some(mint_url) = mint_url {
227            if &self.mint_url != mint_url {
228                return false;
229            }
230        }
231        if let Some(direction) = direction {
232            if &self.direction != direction {
233                return false;
234            }
235        }
236        if let Some(unit) = unit {
237            if &self.unit != unit {
238                return false;
239            }
240        }
241        true
242    }
243}
244
245impl PartialOrd for Transaction {
246    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
247        Some(self.cmp(other))
248    }
249}
250
251impl Ord for Transaction {
252    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
253        self.timestamp
254            .cmp(&other.timestamp)
255            .reverse()
256            .then_with(|| self.id().cmp(&other.id()))
257    }
258}
259
260/// Transaction Direction
261#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub enum TransactionDirection {
263    /// Incoming transaction (i.e., receive or mint)
264    Incoming,
265    /// Outgoing transaction (i.e., send or melt)
266    Outgoing,
267}
268
269impl std::fmt::Display for TransactionDirection {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        match self {
272            TransactionDirection::Incoming => write!(f, "Incoming"),
273            TransactionDirection::Outgoing => write!(f, "Outgoing"),
274        }
275    }
276}
277
278impl FromStr for TransactionDirection {
279    type Err = Error;
280
281    fn from_str(value: &str) -> Result<Self, Self::Err> {
282        match value {
283            "Incoming" => Ok(Self::Incoming),
284            "Outgoing" => Ok(Self::Outgoing),
285            _ => Err(Error::InvalidTransactionDirection),
286        }
287    }
288}
289
290/// Transaction ID
291#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
292#[serde(transparent)]
293pub struct TransactionId([u8; 32]);
294
295impl TransactionId {
296    /// Create new [`TransactionId`]
297    pub fn new(ys: Vec<PublicKey>) -> Self {
298        let mut ys = ys;
299        ys.sort();
300        let mut hasher = sha256::Hash::engine();
301        for y in ys {
302            hasher.input(&y.to_bytes());
303        }
304        let hash = sha256::Hash::from_engine(hasher);
305        Self(hash.to_byte_array())
306    }
307
308    /// From proofs
309    pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
310        let ys = proofs
311            .iter()
312            .map(|proof| proof.y())
313            .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
314        Ok(Self::new(ys))
315    }
316
317    /// From bytes
318    pub fn from_bytes(bytes: [u8; 32]) -> Self {
319        Self(bytes)
320    }
321
322    /// From hex string
323    pub fn from_hex(value: &str) -> Result<Self, Error> {
324        let bytes = hex::decode(value)?;
325        if bytes.len() != 32 {
326            return Err(Error::InvalidTransactionId);
327        }
328        let mut array = [0u8; 32];
329        array.copy_from_slice(&bytes);
330        Ok(Self(array))
331    }
332
333    /// From slice
334    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
335        if slice.len() != 32 {
336            return Err(Error::InvalidTransactionId);
337        }
338        let mut array = [0u8; 32];
339        array.copy_from_slice(slice);
340        Ok(Self(array))
341    }
342
343    /// Get inner value
344    pub fn as_bytes(&self) -> &[u8; 32] {
345        &self.0
346    }
347
348    /// Get inner value as slice
349    pub fn as_slice(&self) -> &[u8] {
350        &self.0
351    }
352}
353
354impl std::fmt::Display for TransactionId {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        write!(f, "{}", hex::encode(self.0))
357    }
358}
359
360impl FromStr for TransactionId {
361    type Err = Error;
362
363    fn from_str(value: &str) -> Result<Self, Self::Err> {
364        Self::from_hex(value)
365    }
366}
367
368impl TryFrom<Proofs> for TransactionId {
369    type Error = nut00::Error;
370
371    fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
372        Self::from_proofs(proofs)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_transaction_id_from_hex() {
382        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
383        let transaction_id = TransactionId::from_hex(hex_str).unwrap();
384        assert_eq!(transaction_id.to_string(), hex_str);
385    }
386
387    #[test]
388    fn test_transaction_id_from_hex_empty_string() {
389        let hex_str = "";
390        let res = TransactionId::from_hex(hex_str);
391        assert!(matches!(res, Err(Error::InvalidTransactionId)));
392    }
393
394    #[test]
395    fn test_transaction_id_from_hex_longer_string() {
396        let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
397        let res = TransactionId::from_hex(hex_str);
398        assert!(matches!(res, Err(Error::InvalidTransactionId)));
399    }
400}