1use crate::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum TransferState {
14 Prepared,
16 Confirmed,
18 Cancelled,
20 WaitingForAdmin,
22 InsufficientFunds,
24 WithdrawalLimit,
26}
27
28impl Default for TransferState {
29 fn default() -> Self {
30 Self::Prepared
31 }
32}
33
34#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum AddressType {
38 Deposit,
40 Withdrawal,
42 Transfer,
44}
45
46impl Default for AddressType {
47 fn default() -> Self {
48 Self::Deposit
49 }
50}
51
52#[derive(Clone, PartialEq, Serialize, Deserialize)]
54pub struct Transfer {
55 pub id: i64,
57 pub currency: String,
59 pub amount: f64,
61 pub fee: f64,
63 pub address: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub transaction_id: Option<String>,
68 pub state: TransferState,
70 pub created_timestamp: i64,
72 pub updated_timestamp: i64,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub confirmed_timestamp: Option<i64>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub transfer_type: Option<String>,
80}
81
82impl Transfer {
83 pub fn new(
85 id: i64,
86 currency: String,
87 amount: f64,
88 fee: f64,
89 address: String,
90 created_timestamp: i64,
91 ) -> Self {
92 Self {
93 id,
94 currency,
95 amount,
96 fee,
97 address,
98 transaction_id: None,
99 state: TransferState::Prepared,
100 created_timestamp,
101 updated_timestamp: created_timestamp,
102 confirmed_timestamp: None,
103 transfer_type: None,
104 }
105 }
106
107 pub fn with_transaction_id(mut self, tx_id: String) -> Self {
109 self.transaction_id = Some(tx_id);
110 self
111 }
112
113 pub fn with_state(mut self, state: TransferState) -> Self {
115 self.state = state;
116 self
117 }
118
119 pub fn with_type(mut self, transfer_type: String) -> Self {
121 self.transfer_type = Some(transfer_type);
122 self
123 }
124
125 pub fn confirm(&mut self, timestamp: i64) {
127 self.state = TransferState::Confirmed;
128 self.confirmed_timestamp = Some(timestamp);
129 self.updated_timestamp = timestamp;
130 }
131
132 pub fn cancel(&mut self, timestamp: i64) {
134 self.state = TransferState::Cancelled;
135 self.updated_timestamp = timestamp;
136 }
137
138 pub fn is_confirmed(&self) -> bool {
140 matches!(self.state, TransferState::Confirmed)
141 }
142
143 pub fn is_cancelled(&self) -> bool {
145 matches!(self.state, TransferState::Cancelled)
146 }
147
148 pub fn is_pending(&self) -> bool {
150 matches!(
151 self.state,
152 TransferState::Prepared | TransferState::WaitingForAdmin
153 )
154 }
155
156 pub fn net_amount(&self) -> f64 {
158 self.amount - self.fee
159 }
160}
161
162impl_json_display!(Transfer);
163impl_json_debug_pretty!(Transfer);
164
165#[derive(Clone, PartialEq, Serialize, Deserialize)]
167pub struct AddressBookEntry {
168 pub address: String,
170 pub currency: String,
172 pub label: String,
174 #[serde(rename = "type")]
176 pub address_type: AddressType,
177 pub requires_confirmation: bool,
179 pub creation_timestamp: i64,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub personal: Option<bool>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub beneficiary_first_name: Option<String>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub beneficiary_last_name: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub beneficiary_address: Option<String>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub beneficiary_vasp_did: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub beneficiary_vasp_name: Option<String>,
199}
200
201impl AddressBookEntry {
202 pub fn new(
204 address: String,
205 currency: String,
206 label: String,
207 address_type: AddressType,
208 creation_timestamp: i64,
209 ) -> Self {
210 Self {
211 address,
212 currency,
213 label,
214 address_type,
215 requires_confirmation: false,
216 creation_timestamp,
217 personal: None,
218 beneficiary_first_name: None,
219 beneficiary_last_name: None,
220 beneficiary_address: None,
221 beneficiary_vasp_did: None,
222 beneficiary_vasp_name: None,
223 }
224 }
225
226 pub fn with_confirmation(mut self, requires: bool) -> Self {
228 self.requires_confirmation = requires;
229 self
230 }
231
232 pub fn with_personal(mut self, personal: bool) -> Self {
234 self.personal = Some(personal);
235 self
236 }
237
238 pub fn with_beneficiary(
240 mut self,
241 first_name: String,
242 last_name: String,
243 address: String,
244 ) -> Self {
245 self.beneficiary_first_name = Some(first_name);
246 self.beneficiary_last_name = Some(last_name);
247 self.beneficiary_address = Some(address);
248 self
249 }
250
251 pub fn with_vasp(mut self, vasp_did: String, vasp_name: String) -> Self {
253 self.beneficiary_vasp_did = Some(vasp_did);
254 self.beneficiary_vasp_name = Some(vasp_name);
255 self
256 }
257
258 pub fn is_withdrawal(&self) -> bool {
260 matches!(self.address_type, AddressType::Withdrawal)
261 }
262
263 pub fn is_deposit(&self) -> bool {
265 matches!(self.address_type, AddressType::Deposit)
266 }
267
268 pub fn is_transfer(&self) -> bool {
270 matches!(self.address_type, AddressType::Transfer)
271 }
272}
273
274impl_json_display!(AddressBookEntry);
275impl_json_debug_pretty!(AddressBookEntry);
276
277#[derive(Clone, PartialEq, Serialize, Deserialize)]
279pub struct SubaccountTransfer {
280 pub amount: f64,
282 pub currency: String,
284 pub destination: i64,
286 pub id: i64,
288 pub source: i64,
290 pub state: TransferState,
292 pub timestamp: i64,
294 pub transfer_type: String,
296}
297
298impl SubaccountTransfer {
299 pub fn new(
301 id: i64,
302 amount: f64,
303 currency: String,
304 source: i64,
305 destination: i64,
306 timestamp: i64,
307 ) -> Self {
308 Self {
309 amount,
310 currency,
311 destination,
312 id,
313 source,
314 state: TransferState::Prepared,
315 timestamp,
316 transfer_type: "subaccount".to_string(),
317 }
318 }
319
320 pub fn with_state(mut self, state: TransferState) -> Self {
322 self.state = state;
323 self
324 }
325
326 pub fn with_type(mut self, transfer_type: String) -> Self {
328 self.transfer_type = transfer_type;
329 self
330 }
331
332 pub fn is_main_subaccount_transfer(&self) -> bool {
334 self.source == 0 || self.destination == 0
335 }
336
337 pub fn is_subaccount_to_subaccount(&self) -> bool {
339 self.source != 0 && self.destination != 0
340 }
341}
342
343impl_json_display!(SubaccountTransfer);
344impl_json_debug_pretty!(SubaccountTransfer);
345
346#[derive(Clone, PartialEq, Serialize, Deserialize)]
348pub struct Transfers {
349 pub transfers: Vec<Transfer>,
351}
352
353impl Transfers {
354 pub fn new() -> Self {
356 Self {
357 transfers: Vec::new(),
358 }
359 }
360
361 pub fn add(&mut self, transfer: Transfer) {
363 self.transfers.push(transfer);
364 }
365
366 pub fn by_currency(&self, currency: String) -> Vec<&Transfer> {
368 self.transfers
369 .iter()
370 .filter(|t| t.currency == currency)
371 .collect()
372 }
373
374 pub fn by_state(&self, state: TransferState) -> Vec<&Transfer> {
376 self.transfers.iter().filter(|t| t.state == state).collect()
377 }
378
379 pub fn pending(&self) -> Vec<&Transfer> {
381 self.transfers.iter().filter(|t| t.is_pending()).collect()
382 }
383
384 pub fn confirmed(&self) -> Vec<&Transfer> {
386 self.transfers.iter().filter(|t| t.is_confirmed()).collect()
387 }
388
389 pub fn total_amount(&self, currency: String) -> f64 {
391 self.transfers
392 .iter()
393 .filter(|t| t.currency == currency)
394 .map(|t| t.amount)
395 .sum()
396 }
397
398 pub fn total_fees(&self, currency: String) -> f64 {
400 self.transfers
401 .iter()
402 .filter(|t| t.currency == currency)
403 .map(|t| t.fee)
404 .sum()
405 }
406}
407
408impl Default for Transfers {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414impl_json_display!(Transfers);
415impl_json_debug_pretty!(Transfers);
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_transfer_creation() {
423 let transfer = Transfer::new(
424 12345,
425 "BTC".to_string(),
426 1.0,
427 0.0005,
428 "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
429 1640995200000,
430 );
431
432 assert_eq!(transfer.id, 12345);
433 assert_eq!(transfer.currency, "BTC");
434 assert_eq!(transfer.amount, 1.0);
435 assert_eq!(transfer.fee, 0.0005);
436 assert_eq!(transfer.net_amount(), 0.9995);
437 assert!(transfer.is_pending());
438 }
439
440 #[test]
441 fn test_transfer_state_changes() {
442 let mut transfer = Transfer::new(
443 1,
444 "BTC".to_string(),
445 1.0,
446 0.001,
447 "address".to_string(),
448 1000,
449 );
450
451 assert!(transfer.is_pending());
452 assert!(!transfer.is_confirmed());
453
454 transfer.confirm(2000);
455 assert!(transfer.is_confirmed());
456 assert!(!transfer.is_pending());
457 assert_eq!(transfer.confirmed_timestamp, Some(2000));
458
459 transfer.cancel(3000);
460 assert!(transfer.is_cancelled());
461 assert_eq!(transfer.updated_timestamp, 3000);
462 }
463
464 #[test]
465 fn test_address_book_entry() {
466 let entry = AddressBookEntry::new(
467 "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
468 "BTC".to_string(),
469 "Main wallet".to_string(),
470 AddressType::Withdrawal,
471 1640995200000,
472 )
473 .with_confirmation(true)
474 .with_personal(false)
475 .with_beneficiary(
476 "John".to_string(),
477 "Doe".to_string(),
478 "123 Main St".to_string(),
479 );
480
481 assert!(entry.is_withdrawal());
482 assert!(!entry.is_deposit());
483 assert!(entry.requires_confirmation);
484 assert_eq!(entry.beneficiary_first_name, Some("John".to_string()));
485 }
486
487 #[test]
488 fn test_subaccount_transfer() {
489 let transfer = SubaccountTransfer::new(
490 1,
491 100.0,
492 "BTC".to_string(),
493 0, 123, 1640995200000,
496 );
497
498 assert!(transfer.is_main_subaccount_transfer());
499 assert!(!transfer.is_subaccount_to_subaccount());
500 }
501
502 #[test]
503 fn test_transfers_collection() {
504 let mut transfers = Transfers::new();
505
506 transfers.add(
507 Transfer::new(1, "BTC".to_string(), 1.0, 0.001, "addr1".to_string(), 1000)
508 .with_state(TransferState::Confirmed),
509 );
510
511 transfers.add(Transfer::new(
512 2,
513 "BTC".to_string(),
514 0.5,
515 0.0005,
516 "addr2".to_string(),
517 2000,
518 ));
519
520 assert_eq!(transfers.transfers.len(), 2);
521 assert_eq!(transfers.by_currency("BTC".to_string()).len(), 2);
522 assert_eq!(transfers.confirmed().len(), 1);
523 assert_eq!(transfers.pending().len(), 1);
524 assert_eq!(transfers.total_amount("BTC".to_string()), 1.5);
525 assert_eq!(transfers.total_fees("BTC".to_string()), 0.0015);
526 }
527
528 #[test]
529 fn test_serde() {
530 let transfer = Transfer::new(
531 12345,
532 "BTC".to_string(),
533 1.0,
534 0.0005,
535 "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
536 1640995200000,
537 )
538 .with_transaction_id("tx123".to_string())
539 .with_state(TransferState::Confirmed);
540
541 let json = serde_json::to_string(&transfer).unwrap();
542 let deserialized: Transfer = serde_json::from_str(&json).unwrap();
543 assert_eq!(transfer, deserialized);
544 }
545}