Skip to main content

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