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