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, 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#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18pub struct WalletKey {
19 pub mint_url: MintUrl,
21 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 pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
34 Self { mint_url, unit }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct MintQuote {
41 pub id: String,
43 pub mint_url: MintUrl,
45 #[serde(default)]
47 pub payment_method: PaymentMethod,
48 pub amount: Option<Amount>,
50 pub unit: CurrencyUnit,
52 pub request: String,
54 pub state: MintQuoteState,
56 pub expiry: u64,
58 pub secret_key: Option<SecretKey>,
60 #[serde(default)]
62 pub amount_issued: Amount,
63 #[serde(default)]
65 pub amount_paid: Amount,
66}
67
68#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
70pub struct MeltQuote {
71 pub id: String,
73 pub unit: CurrencyUnit,
75 pub amount: Amount,
77 pub request: String,
79 pub fee_reserve: Amount,
81 pub state: MeltQuoteState,
83 pub expiry: u64,
85 pub payment_preimage: Option<String>,
87}
88
89impl MintQuote {
90 #[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 pub fn total_amount(&self) -> Amount {
119 self.amount_paid
120 }
121
122 pub fn is_expired(&self, current_time: u64) -> bool {
124 current_time > self.expiry
125 }
126
127 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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
147pub enum SendKind {
148 #[default]
149 OnlineExact,
151 OnlineTolerance(Amount),
153 OfflineExact,
155 OfflineTolerance(Amount),
157}
158
159impl SendKind {
160 pub fn is_online(&self) -> bool {
162 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
163 }
164
165 pub fn is_offline(&self) -> bool {
167 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
168 }
169
170 pub fn is_exact(&self) -> bool {
172 matches!(self, Self::OnlineExact | Self::OfflineExact)
173 }
174
175 pub fn has_tolerance(&self) -> bool {
177 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183pub struct Transaction {
184 pub mint_url: MintUrl,
186 pub direction: TransactionDirection,
188 pub amount: Amount,
190 pub fee: Amount,
192 pub unit: CurrencyUnit,
194 pub ys: Vec<PublicKey>,
196 pub timestamp: u64,
198 pub memo: Option<String>,
200 pub metadata: HashMap<String, String>,
202}
203
204impl Transaction {
205 pub fn id(&self) -> TransactionId {
207 TransactionId::new(self.ys.clone())
208 }
209
210 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub enum TransactionDirection {
251 Incoming,
253 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
280#[serde(transparent)]
281pub struct TransactionId([u8; 32]);
282
283impl TransactionId {
284 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 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 pub fn from_bytes(bytes: [u8; 32]) -> Self {
307 Self(bytes)
308 }
309
310 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 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 pub fn as_bytes(&self) -> &[u8; 32] {
333 &self.0
334 }
335
336 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}