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::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10/// Transfer state enumeration
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum TransferState {
14    /// Transfer is prepared but not yet confirmed
15    Prepared,
16    /// Transfer has been confirmed
17    Confirmed,
18    /// Transfer has been cancelled
19    Cancelled,
20    /// Transfer is waiting for admin approval
21    WaitingForAdmin,
22    /// Transfer failed due to insufficient funds
23    InsufficientFunds,
24    /// Transfer failed due to withdrawal limit
25    WithdrawalLimit,
26}
27
28impl Default for TransferState {
29    fn default() -> Self {
30        Self::Prepared
31    }
32}
33
34/// Address type enumeration
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum AddressType {
38    /// Deposit address
39    Deposit,
40    /// Withdrawal address
41    Withdrawal,
42    /// Transfer address
43    Transfer,
44}
45
46impl Default for AddressType {
47    fn default() -> Self {
48        Self::Deposit
49    }
50}
51
52/// Transfer information
53#[derive(Clone, PartialEq, Serialize, Deserialize)]
54pub struct Transfer {
55    /// Transfer ID
56    pub id: i64,
57    /// Currency being transferred
58    pub currency: String,
59    /// Transfer amount
60    pub amount: f64,
61    /// Transfer fee
62    pub fee: f64,
63    /// Destination address
64    pub address: String,
65    /// Blockchain transaction ID
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub transaction_id: Option<String>,
68    /// Current transfer state
69    pub state: TransferState,
70    /// Creation timestamp (milliseconds since Unix epoch)
71    pub created_timestamp: i64,
72    /// Last update timestamp (milliseconds since Unix epoch)
73    pub updated_timestamp: i64,
74    /// Confirmation timestamp (milliseconds since Unix epoch)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub confirmed_timestamp: Option<i64>,
77    /// Transfer type description
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub transfer_type: Option<String>,
80}
81
82impl Transfer {
83    /// Create a new transfer
84    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    /// Set transaction ID
108    pub fn with_transaction_id(mut self, tx_id: String) -> Self {
109        self.transaction_id = Some(tx_id);
110        self
111    }
112
113    /// Set transfer state
114    pub fn with_state(mut self, state: TransferState) -> Self {
115        self.state = state;
116        self
117    }
118
119    /// Set transfer type
120    pub fn with_type(mut self, transfer_type: String) -> Self {
121        self.transfer_type = Some(transfer_type);
122        self
123    }
124
125    /// Confirm the transfer
126    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    /// Cancel the transfer
133    pub fn cancel(&mut self, timestamp: i64) {
134        self.state = TransferState::Cancelled;
135        self.updated_timestamp = timestamp;
136    }
137
138    /// Check if transfer is confirmed
139    pub fn is_confirmed(&self) -> bool {
140        matches!(self.state, TransferState::Confirmed)
141    }
142
143    /// Check if transfer is cancelled
144    pub fn is_cancelled(&self) -> bool {
145        matches!(self.state, TransferState::Cancelled)
146    }
147
148    /// Check if transfer is pending
149    pub fn is_pending(&self) -> bool {
150        matches!(
151            self.state,
152            TransferState::Prepared | TransferState::WaitingForAdmin
153        )
154    }
155
156    /// Get net amount (amount - fee)
157    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/// Address book entry
166#[derive(Clone, PartialEq, Serialize, Deserialize)]
167pub struct AddressBookEntry {
168    /// Cryptocurrency address
169    pub address: String,
170    /// Currency for this address
171    pub currency: String,
172    /// User-defined label for the address
173    pub label: String,
174    /// Type of address
175    #[serde(rename = "type")]
176    pub address_type: AddressType,
177    /// Whether this address requires email confirmation for withdrawals
178    pub requires_confirmation: bool,
179    /// Creation timestamp (milliseconds since Unix epoch)
180    pub creation_timestamp: i64,
181    /// Whether this is a personal address
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub personal: Option<bool>,
184    /// Beneficiary information for compliance
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub beneficiary_first_name: Option<String>,
187    /// Beneficiary last name
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub beneficiary_last_name: Option<String>,
190    /// Beneficiary address for compliance
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub beneficiary_address: Option<String>,
193    /// Beneficiary VASP DID
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub beneficiary_vasp_did: Option<String>,
196    /// Beneficiary VASP name
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub beneficiary_vasp_name: Option<String>,
199}
200
201impl AddressBookEntry {
202    /// Create a new address book entry
203    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    /// Set confirmation requirement
227    pub fn with_confirmation(mut self, requires: bool) -> Self {
228        self.requires_confirmation = requires;
229        self
230    }
231
232    /// Set personal flag
233    pub fn with_personal(mut self, personal: bool) -> Self {
234        self.personal = Some(personal);
235        self
236    }
237
238    /// Set beneficiary information
239    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    /// Set VASP information
252    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    /// Check if this is a withdrawal address
259    pub fn is_withdrawal(&self) -> bool {
260        matches!(self.address_type, AddressType::Withdrawal)
261    }
262
263    /// Check if this is a deposit address
264    pub fn is_deposit(&self) -> bool {
265        matches!(self.address_type, AddressType::Deposit)
266    }
267
268    /// Check if this is a transfer address
269    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/// Subaccount transfer information
278#[derive(Clone, PartialEq, Serialize, Deserialize)]
279pub struct SubaccountTransfer {
280    /// Transfer amount
281    pub amount: f64,
282    /// Currency being transferred
283    pub currency: String,
284    /// Destination subaccount ID
285    pub destination: i64,
286    /// Transfer ID
287    pub id: i64,
288    /// Source subaccount ID
289    pub source: i64,
290    /// Transfer state
291    pub state: TransferState,
292    /// Transfer timestamp (milliseconds since Unix epoch)
293    pub timestamp: i64,
294    /// Type of transfer
295    pub transfer_type: String,
296}
297
298impl SubaccountTransfer {
299    /// Create a new subaccount transfer
300    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    /// Set transfer state
321    pub fn with_state(mut self, state: TransferState) -> Self {
322        self.state = state;
323        self
324    }
325
326    /// Set transfer type
327    pub fn with_type(mut self, transfer_type: String) -> Self {
328        self.transfer_type = transfer_type;
329        self
330    }
331
332    /// Check if transfer is between main account and subaccount
333    pub fn is_main_subaccount_transfer(&self) -> bool {
334        self.source == 0 || self.destination == 0
335    }
336
337    /// Check if transfer is between subaccounts
338    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/// Collection of transfers
347#[derive(Clone, PartialEq, Serialize, Deserialize)]
348pub struct Transfers {
349    /// List of transfers
350    pub transfers: Vec<Transfer>,
351}
352
353impl Transfers {
354    /// Create a new transfers collection
355    pub fn new() -> Self {
356        Self {
357            transfers: Vec::new(),
358        }
359    }
360
361    /// Add a transfer
362    pub fn add(&mut self, transfer: Transfer) {
363        self.transfers.push(transfer);
364    }
365
366    /// Get transfers by currency
367    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    /// Get transfers by state
375    pub fn by_state(&self, state: TransferState) -> Vec<&Transfer> {
376        self.transfers.iter().filter(|t| t.state == state).collect()
377    }
378
379    /// Get pending transfers
380    pub fn pending(&self) -> Vec<&Transfer> {
381        self.transfers.iter().filter(|t| t.is_pending()).collect()
382    }
383
384    /// Get confirmed transfers
385    pub fn confirmed(&self) -> Vec<&Transfer> {
386        self.transfers.iter().filter(|t| t.is_confirmed()).collect()
387    }
388
389    /// Calculate total amount by currency
390    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    /// Calculate total fees by currency
399    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,   // main account
494            123, // subaccount
495            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}