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