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 #[serde(default)]
89 pub payment_method: PaymentMethod,
90}
91
92impl MintQuote {
93 #[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 pub fn total_amount(&self) -> Amount {
122 self.amount_paid
123 }
124
125 pub fn is_expired(&self, current_time: u64) -> bool {
127 current_time > self.expiry
128 }
129
130 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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
150pub enum SendKind {
151 #[default]
152 OnlineExact,
154 OnlineTolerance(Amount),
156 OfflineExact,
158 OfflineTolerance(Amount),
160}
161
162impl SendKind {
163 pub fn is_online(&self) -> bool {
165 matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
166 }
167
168 pub fn is_offline(&self) -> bool {
170 matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
171 }
172
173 pub fn is_exact(&self) -> bool {
175 matches!(self, Self::OnlineExact | Self::OfflineExact)
176 }
177
178 pub fn has_tolerance(&self) -> bool {
180 matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct Transaction {
187 pub mint_url: MintUrl,
189 pub direction: TransactionDirection,
191 pub amount: Amount,
193 pub fee: Amount,
195 pub unit: CurrencyUnit,
197 pub ys: Vec<PublicKey>,
199 pub timestamp: u64,
201 pub memo: Option<String>,
203 pub metadata: HashMap<String, String>,
205 pub quote_id: Option<String>,
207 pub payment_request: Option<String>,
209 pub payment_proof: Option<String>,
211}
212
213impl Transaction {
214 pub fn id(&self) -> TransactionId {
216 TransactionId::new(self.ys.clone())
217 }
218
219 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub enum TransactionDirection {
263 Incoming,
265 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
292#[serde(transparent)]
293pub struct TransactionId([u8; 32]);
294
295impl TransactionId {
296 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 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 pub fn from_bytes(bytes: [u8; 32]) -> Self {
319 Self(bytes)
320 }
321
322 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 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 pub fn as_bytes(&self) -> &[u8; 32] {
345 &self.0
346 }
347
348 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}