deribit_base/model/
transfer.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6
7use crate::prelude::Currency;
8use crate::{impl_json_debug_pretty, impl_json_display};
9use serde::{Deserialize, Serialize};
10
11/// Transfer state enumeration
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum TransferState {
15    /// Transfer is prepared but not yet confirmed
16    Prepared,
17    /// Transfer has been confirmed
18    Confirmed,
19    /// Transfer has been cancelled
20    Cancelled,
21    /// Transfer is waiting for admin approval
22    WaitingForAdmin,
23    /// Transfer failed due to insufficient funds
24    InsufficientFunds,
25    /// Transfer failed due to withdrawal limit
26    WithdrawalLimit,
27}
28
29impl Default for TransferState {
30    fn default() -> Self {
31        Self::Prepared
32    }
33}
34
35/// Address type enumeration
36#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum AddressType {
39    /// Deposit address
40    Deposit,
41    /// Withdrawal address
42    Withdrawal,
43    /// Transfer address
44    Transfer,
45}
46
47impl Default for AddressType {
48    fn default() -> Self {
49        Self::Deposit
50    }
51}
52
53/// Transfer information
54#[derive(Clone, PartialEq, Serialize, Deserialize)]
55pub struct Transfer {
56    /// Transfer ID
57    pub id: i64,
58    /// Currency being transferred
59    pub currency: Currency,
60    /// Transfer amount
61    pub amount: f64,
62    /// Transfer fee
63    pub fee: f64,
64    /// Destination address
65    pub address: String,
66    /// Blockchain transaction ID
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub transaction_id: Option<String>,
69    /// Current transfer state
70    pub state: TransferState,
71    /// Creation timestamp (milliseconds since Unix epoch)
72    pub created_timestamp: i64,
73    /// Last update timestamp (milliseconds since Unix epoch)
74    pub updated_timestamp: i64,
75    /// Confirmation timestamp (milliseconds since Unix epoch)
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub confirmed_timestamp: Option<i64>,
78    /// Transfer type description
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub transfer_type: Option<String>,
81}
82
83impl Transfer {
84    /// Create a new transfer
85    pub fn new(
86        id: i64,
87        currency: Currency,
88        amount: f64,
89        fee: f64,
90        address: String,
91        created_timestamp: i64,
92    ) -> Self {
93        Self {
94            id,
95            currency,
96            amount,
97            fee,
98            address,
99            transaction_id: None,
100            state: TransferState::Prepared,
101            created_timestamp,
102            updated_timestamp: created_timestamp,
103            confirmed_timestamp: None,
104            transfer_type: None,
105        }
106    }
107
108    /// Set transaction ID
109    pub fn with_transaction_id(mut self, tx_id: String) -> Self {
110        self.transaction_id = Some(tx_id);
111        self
112    }
113
114    /// Set transfer state
115    pub fn with_state(mut self, state: TransferState) -> Self {
116        self.state = state;
117        self
118    }
119
120    /// Set transfer type
121    pub fn with_type(mut self, transfer_type: String) -> Self {
122        self.transfer_type = Some(transfer_type);
123        self
124    }
125
126    /// Confirm the transfer
127    pub fn confirm(&mut self, timestamp: i64) {
128        self.state = TransferState::Confirmed;
129        self.confirmed_timestamp = Some(timestamp);
130        self.updated_timestamp = timestamp;
131    }
132
133    /// Cancel the transfer
134    pub fn cancel(&mut self, timestamp: i64) {
135        self.state = TransferState::Cancelled;
136        self.updated_timestamp = timestamp;
137    }
138
139    /// Check if transfer is confirmed
140    pub fn is_confirmed(&self) -> bool {
141        matches!(self.state, TransferState::Confirmed)
142    }
143
144    /// Check if transfer is cancelled
145    pub fn is_cancelled(&self) -> bool {
146        matches!(self.state, TransferState::Cancelled)
147    }
148
149    /// Check if transfer is pending
150    pub fn is_pending(&self) -> bool {
151        matches!(
152            self.state,
153            TransferState::Prepared | TransferState::WaitingForAdmin
154        )
155    }
156
157    /// Get net amount (amount - fee)
158    pub fn net_amount(&self) -> f64 {
159        self.amount - self.fee
160    }
161}
162
163impl_json_display!(Transfer);
164impl_json_debug_pretty!(Transfer);
165
166/// Address book entry
167#[derive(Clone, PartialEq, Serialize, Deserialize)]
168pub struct AddressBookEntry {
169    /// Cryptocurrency address
170    pub address: String,
171    /// Currency for this address
172    pub currency: Currency,
173    /// User-defined label for the address
174    pub label: String,
175    /// Type of address
176    #[serde(rename = "type")]
177    pub address_type: AddressType,
178    /// Whether this address requires email confirmation for withdrawals
179    pub requires_confirmation: bool,
180    /// Creation timestamp (milliseconds since Unix epoch)
181    pub creation_timestamp: i64,
182    /// Whether this is a personal address
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub personal: Option<bool>,
185    /// Beneficiary information for compliance
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub beneficiary_first_name: Option<String>,
188    /// Beneficiary last name
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub beneficiary_last_name: Option<String>,
191    /// Beneficiary address for compliance
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub beneficiary_address: Option<String>,
194    /// Beneficiary VASP DID
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub beneficiary_vasp_did: Option<String>,
197    /// Beneficiary VASP name
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub beneficiary_vasp_name: Option<String>,
200}
201
202impl AddressBookEntry {
203    /// Create a new address book entry
204    pub fn new(
205        address: String,
206        currency: Currency,
207        label: String,
208        address_type: AddressType,
209        creation_timestamp: i64,
210    ) -> Self {
211        Self {
212            address,
213            currency,
214            label,
215            address_type,
216            requires_confirmation: false,
217            creation_timestamp,
218            personal: None,
219            beneficiary_first_name: None,
220            beneficiary_last_name: None,
221            beneficiary_address: None,
222            beneficiary_vasp_did: None,
223            beneficiary_vasp_name: None,
224        }
225    }
226
227    /// Set confirmation requirement
228    pub fn with_confirmation(mut self, requires: bool) -> Self {
229        self.requires_confirmation = requires;
230        self
231    }
232
233    /// Set personal flag
234    pub fn with_personal(mut self, personal: bool) -> Self {
235        self.personal = Some(personal);
236        self
237    }
238
239    /// Set beneficiary information
240    pub fn with_beneficiary(
241        mut self,
242        first_name: String,
243        last_name: String,
244        address: String,
245    ) -> Self {
246        self.beneficiary_first_name = Some(first_name);
247        self.beneficiary_last_name = Some(last_name);
248        self.beneficiary_address = Some(address);
249        self
250    }
251
252    /// Set VASP information
253    pub fn with_vasp(mut self, vasp_did: String, vasp_name: String) -> Self {
254        self.beneficiary_vasp_did = Some(vasp_did);
255        self.beneficiary_vasp_name = Some(vasp_name);
256        self
257    }
258
259    /// Check if this is a withdrawal address
260    pub fn is_withdrawal(&self) -> bool {
261        matches!(self.address_type, AddressType::Withdrawal)
262    }
263
264    /// Check if this is a deposit address
265    pub fn is_deposit(&self) -> bool {
266        matches!(self.address_type, AddressType::Deposit)
267    }
268
269    /// Check if this is a transfer address
270    pub fn is_transfer(&self) -> bool {
271        matches!(self.address_type, AddressType::Transfer)
272    }
273}
274
275impl_json_display!(AddressBookEntry);
276impl_json_debug_pretty!(AddressBookEntry);
277
278/// Subaccount transfer information
279#[derive(Clone, PartialEq, Serialize, Deserialize)]
280pub struct SubaccountTransfer {
281    /// Transfer amount
282    pub amount: f64,
283    /// Currency being transferred
284    pub currency: Currency,
285    /// Destination subaccount ID
286    pub destination: i64,
287    /// Transfer ID
288    pub id: i64,
289    /// Source subaccount ID
290    pub source: i64,
291    /// Transfer state
292    pub state: TransferState,
293    /// Transfer timestamp (milliseconds since Unix epoch)
294    pub timestamp: i64,
295    /// Type of transfer
296    pub transfer_type: String,
297}
298
299impl SubaccountTransfer {
300    /// Create a new subaccount transfer
301    pub fn new(
302        id: i64,
303        amount: f64,
304        currency: Currency,
305        source: i64,
306        destination: i64,
307        timestamp: i64,
308    ) -> Self {
309        Self {
310            amount,
311            currency,
312            destination,
313            id,
314            source,
315            state: TransferState::Prepared,
316            timestamp,
317            transfer_type: "subaccount".to_string(),
318        }
319    }
320
321    /// Set transfer state
322    pub fn with_state(mut self, state: TransferState) -> Self {
323        self.state = state;
324        self
325    }
326
327    /// Set transfer type
328    pub fn with_type(mut self, transfer_type: String) -> Self {
329        self.transfer_type = transfer_type;
330        self
331    }
332
333    /// Check if transfer is between main account and subaccount
334    pub fn is_main_subaccount_transfer(&self) -> bool {
335        self.source == 0 || self.destination == 0
336    }
337
338    /// Check if transfer is between subaccounts
339    pub fn is_subaccount_to_subaccount(&self) -> bool {
340        self.source != 0 && self.destination != 0
341    }
342}
343
344impl_json_display!(SubaccountTransfer);
345impl_json_debug_pretty!(SubaccountTransfer);
346
347/// Collection of transfers
348#[derive(Clone, PartialEq, Serialize, Deserialize)]
349pub struct Transfers {
350    /// List of transfers
351    pub transfers: Vec<Transfer>,
352}
353
354impl Transfers {
355    /// Create a new transfers collection
356    pub fn new() -> Self {
357        Self {
358            transfers: Vec::new(),
359        }
360    }
361
362    /// Add a transfer
363    pub fn add(&mut self, transfer: Transfer) {
364        self.transfers.push(transfer);
365    }
366
367    /// Get transfers by currency
368    pub fn by_currency(&self, currency: Currency) -> Vec<&Transfer> {
369        self.transfers
370            .iter()
371            .filter(|t| t.currency == currency)
372            .collect()
373    }
374
375    /// Get transfers by state
376    pub fn by_state(&self, state: TransferState) -> Vec<&Transfer> {
377        self.transfers.iter().filter(|t| t.state == state).collect()
378    }
379
380    /// Get pending transfers
381    pub fn pending(&self) -> Vec<&Transfer> {
382        self.transfers.iter().filter(|t| t.is_pending()).collect()
383    }
384
385    /// Get confirmed transfers
386    pub fn confirmed(&self) -> Vec<&Transfer> {
387        self.transfers.iter().filter(|t| t.is_confirmed()).collect()
388    }
389
390    /// Calculate total amount by currency
391    pub fn total_amount(&self, currency: Currency) -> f64 {
392        self.transfers
393            .iter()
394            .filter(|t| t.currency == currency)
395            .map(|t| t.amount)
396            .sum()
397    }
398
399    /// Calculate total fees by currency
400    pub fn total_fees(&self, currency: Currency) -> f64 {
401        self.transfers
402            .iter()
403            .filter(|t| t.currency == currency)
404            .map(|t| t.fee)
405            .sum()
406    }
407}
408
409impl Default for Transfers {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415impl_json_display!(Transfers);
416impl_json_debug_pretty!(Transfers);
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[test]
423    fn test_transfer_creation() {
424        let transfer = Transfer::new(
425            12345,
426            Currency::Bitcoin,
427            1.0,
428            0.0005,
429            "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
430            1640995200000,
431        );
432
433        assert_eq!(transfer.id, 12345);
434        assert_eq!(transfer.currency, Currency::Bitcoin);
435        assert_eq!(transfer.amount, 1.0);
436        assert_eq!(transfer.fee, 0.0005);
437        assert_eq!(transfer.net_amount(), 0.9995);
438        assert!(transfer.is_pending());
439    }
440
441    #[test]
442    fn test_transfer_state_changes() {
443        let mut transfer = Transfer::new(
444            1,
445            Currency::Bitcoin,
446            1.0,
447            0.001,
448            "address".to_string(),
449            1000,
450        );
451
452        assert!(transfer.is_pending());
453        assert!(!transfer.is_confirmed());
454
455        transfer.confirm(2000);
456        assert!(transfer.is_confirmed());
457        assert!(!transfer.is_pending());
458        assert_eq!(transfer.confirmed_timestamp, Some(2000));
459
460        transfer.cancel(3000);
461        assert!(transfer.is_cancelled());
462        assert_eq!(transfer.updated_timestamp, 3000);
463    }
464
465    #[test]
466    fn test_address_book_entry() {
467        let entry = AddressBookEntry::new(
468            "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
469            Currency::Bitcoin,
470            "Main wallet".to_string(),
471            AddressType::Withdrawal,
472            1640995200000,
473        )
474        .with_confirmation(true)
475        .with_personal(false)
476        .with_beneficiary(
477            "John".to_string(),
478            "Doe".to_string(),
479            "123 Main St".to_string(),
480        );
481
482        assert!(entry.is_withdrawal());
483        assert!(!entry.is_deposit());
484        assert!(entry.requires_confirmation);
485        assert_eq!(entry.beneficiary_first_name, Some("John".to_string()));
486    }
487
488    #[test]
489    fn test_subaccount_transfer() {
490        let transfer = SubaccountTransfer::new(
491            1,
492            100.0,
493            Currency::Bitcoin,
494            0,   // main account
495            123, // subaccount
496            1640995200000,
497        );
498
499        assert!(transfer.is_main_subaccount_transfer());
500        assert!(!transfer.is_subaccount_to_subaccount());
501    }
502
503    #[test]
504    fn test_transfers_collection() {
505        let mut transfers = Transfers::new();
506
507        transfers.add(
508            Transfer::new(1, Currency::Bitcoin, 1.0, 0.001, "addr1".to_string(), 1000)
509                .with_state(TransferState::Confirmed),
510        );
511
512        transfers.add(Transfer::new(
513            2,
514            Currency::Bitcoin,
515            0.5,
516            0.0005,
517            "addr2".to_string(),
518            2000,
519        ));
520
521        assert_eq!(transfers.transfers.len(), 2);
522        assert_eq!(transfers.by_currency(Currency::Bitcoin).len(), 2);
523        assert_eq!(transfers.confirmed().len(), 1);
524        assert_eq!(transfers.pending().len(), 1);
525        assert_eq!(transfers.total_amount(Currency::Bitcoin), 1.5);
526        assert_eq!(transfers.total_fees(Currency::Bitcoin), 0.0015);
527    }
528
529    #[test]
530    fn test_serde() {
531        let transfer = Transfer::new(
532            12345,
533            Currency::Bitcoin,
534            1.0,
535            0.0005,
536            "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
537            1640995200000,
538        )
539        .with_transaction_id("tx123".to_string())
540        .with_state(TransferState::Confirmed);
541
542        let json = serde_json::to_string(&transfer).unwrap();
543        let deserialized: Transfer = serde_json::from_str(&json).unwrap();
544        assert_eq!(transfer, deserialized);
545    }
546}