Skip to main content

deribit_base/model/
wallet.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 6/3/26
5******************************************************************************/
6
7//! Wallet-related data structures and types
8//!
9//! This module contains types for Deribit wallet operations including
10//! deposit addresses, withdrawals, and clearance/compliance information.
11
12use crate::model::transfer::AddressType;
13use pretty_simple_display::{DebugPretty, DisplaySimple};
14use serde::{Deserialize, Serialize};
15
16/// Withdrawal state enumeration
17///
18/// Represents the current state of a withdrawal request.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
20#[serde(rename_all = "snake_case")]
21pub enum WithdrawalState {
22    /// Withdrawal is unconfirmed (awaiting email confirmation)
23    #[default]
24    Unconfirmed,
25    /// Withdrawal has been confirmed by user
26    Confirmed,
27    /// Withdrawal has been cancelled
28    Cancelled,
29    /// Withdrawal has been completed successfully
30    Completed,
31    /// Withdrawal was interrupted
32    Interrupted,
33    /// Withdrawal was rejected
34    Rejected,
35}
36
37impl WithdrawalState {
38    /// Get the string representation for API requests
39    #[must_use]
40    pub fn as_str(&self) -> &'static str {
41        match self {
42            Self::Unconfirmed => "unconfirmed",
43            Self::Confirmed => "confirmed",
44            Self::Cancelled => "cancelled",
45            Self::Completed => "completed",
46            Self::Interrupted => "interrupted",
47            Self::Rejected => "rejected",
48        }
49    }
50
51    /// Check if the withdrawal is in a terminal state
52    #[must_use]
53    pub fn is_terminal(&self) -> bool {
54        matches!(
55            self,
56            Self::Cancelled | Self::Completed | Self::Interrupted | Self::Rejected
57        )
58    }
59
60    /// Check if the withdrawal was successful
61    #[must_use]
62    pub fn is_successful(&self) -> bool {
63        matches!(self, Self::Completed)
64    }
65}
66
67impl std::fmt::Display for WithdrawalState {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{}", self.as_str())
70    }
71}
72
73/// Withdrawal priority for BTC transactions
74///
75/// Higher priority results in faster confirmation but higher fees.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
77#[serde(rename_all = "snake_case")]
78pub enum WithdrawalPriority {
79    /// Insane priority - fastest, highest fee
80    Insane,
81    /// Extreme high priority
82    ExtremeHigh,
83    /// Very high priority
84    VeryHigh,
85    /// High priority (default)
86    #[default]
87    High,
88    /// Medium priority
89    Mid,
90    /// Low priority
91    Low,
92    /// Very low priority - slowest, lowest fee
93    VeryLow,
94}
95
96impl WithdrawalPriority {
97    /// Get the string representation for API requests
98    #[must_use]
99    pub fn as_str(&self) -> &'static str {
100        match self {
101            Self::Insane => "insane",
102            Self::ExtremeHigh => "extreme_high",
103            Self::VeryHigh => "very_high",
104            Self::High => "high",
105            Self::Mid => "mid",
106            Self::Low => "low",
107            Self::VeryLow => "very_low",
108        }
109    }
110}
111
112impl std::fmt::Display for WithdrawalPriority {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.as_str())
115    }
116}
117
118/// Deposit address information
119///
120/// Represents a deposit address for a specific currency,
121/// returned by `/private/get_current_deposit_address` or `/private/create_deposit_address`.
122#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct DepositAddress {
124    /// Cryptocurrency address in proper format
125    pub address: String,
126    /// Currency code (e.g., "BTC", "ETH")
127    pub currency: String,
128    /// Address type (deposit/withdrawal/transfer)
129    #[serde(rename = "type")]
130    pub address_type: AddressType,
131    /// Creation timestamp in milliseconds since Unix epoch
132    pub creation_timestamp: i64,
133}
134
135impl DepositAddress {
136    /// Create a new deposit address
137    #[must_use]
138    pub fn new(address: String, currency: String, creation_timestamp: i64) -> Self {
139        Self {
140            address,
141            currency,
142            address_type: AddressType::Deposit,
143            creation_timestamp,
144        }
145    }
146}
147
148/// Withdrawal request
149///
150/// Used to submit a withdrawal via `/private/withdraw`.
151#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
152pub struct WithdrawalRequest {
153    /// Currency to withdraw (e.g., "BTC", "ETH")
154    pub currency: String,
155    /// Destination address (must be in address book)
156    pub address: String,
157    /// Amount to withdraw
158    pub amount: f64,
159    /// Withdrawal priority (optional, BTC only)
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub priority: Option<WithdrawalPriority>,
162}
163
164impl WithdrawalRequest {
165    /// Create a new withdrawal request
166    #[must_use]
167    pub fn new(currency: String, address: String, amount: f64) -> Self {
168        Self {
169            currency,
170            address,
171            amount,
172            priority: None,
173        }
174    }
175
176    /// Set withdrawal priority (BTC only)
177    #[must_use]
178    pub fn with_priority(mut self, priority: WithdrawalPriority) -> Self {
179        self.priority = Some(priority);
180        self
181    }
182}
183
184/// Deposit identification for clearance operations
185///
186/// Used to identify a specific deposit when setting clearance originator
187/// via `/private/set_clearance_originator`.
188#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct DepositId {
190    /// Currency code
191    pub currency: String,
192    /// User/subaccount ID
193    pub user_id: i64,
194    /// Deposit address
195    pub address: String,
196    /// Blockchain transaction hash
197    pub tx_hash: String,
198}
199
200impl DepositId {
201    /// Create a new deposit ID
202    #[must_use]
203    pub fn new(currency: String, user_id: i64, address: String, tx_hash: String) -> Self {
204        Self {
205            currency,
206            user_id,
207            address,
208            tx_hash,
209        }
210    }
211}
212
213/// Clearance originator information for compliance
214///
215/// Contains information about the originator of a deposit
216/// for regulatory compliance purposes.
217#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct ClearanceOriginator {
219    /// Whether the user is the originator (self-transfer)
220    pub is_personal: bool,
221    /// First name (if originator is a person)
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub first_name: Option<String>,
224    /// Last name (if originator is a person)
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub last_name: Option<String>,
227    /// Company name (if originator is a legal entity)
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub company_name: Option<String>,
230    /// Geographical address of the originator
231    pub address: String,
232}
233
234impl ClearanceOriginator {
235    /// Create a personal originator (self-transfer)
236    #[must_use]
237    pub fn personal(address: String) -> Self {
238        Self {
239            is_personal: true,
240            first_name: None,
241            last_name: None,
242            company_name: None,
243            address,
244        }
245    }
246
247    /// Create an individual originator
248    #[must_use]
249    pub fn individual(first_name: String, last_name: String, address: String) -> Self {
250        Self {
251            is_personal: false,
252            first_name: Some(first_name),
253            last_name: Some(last_name),
254            company_name: None,
255            address,
256        }
257    }
258
259    /// Create a company originator
260    #[must_use]
261    pub fn company(company_name: String, address: String) -> Self {
262        Self {
263            is_personal: false,
264            first_name: None,
265            last_name: None,
266            company_name: Some(company_name),
267            address,
268        }
269    }
270
271    /// Check if this is a self-transfer
272    #[must_use]
273    pub fn is_self_transfer(&self) -> bool {
274        self.is_personal
275    }
276
277    /// Check if originator is an individual
278    #[must_use]
279    pub fn is_individual(&self) -> bool {
280        !self.is_personal && self.first_name.is_some() && self.last_name.is_some()
281    }
282
283    /// Check if originator is a company
284    #[must_use]
285    pub fn is_company(&self) -> bool {
286        !self.is_personal && self.company_name.is_some()
287    }
288}
289
290/// Clearance state for deposits
291///
292/// Represents the clearance/compliance state of a deposit.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
294#[serde(rename_all = "snake_case")]
295pub enum ClearanceState {
296    /// Clearance not required
297    #[default]
298    NotRequired,
299    /// Clearance is in progress
300    InProgress,
301    /// Clearance has been completed
302    Completed,
303    /// Clearance failed
304    Failed,
305}
306
307impl ClearanceState {
308    /// Get the string representation
309    #[must_use]
310    pub fn as_str(&self) -> &'static str {
311        match self {
312            Self::NotRequired => "not_required",
313            Self::InProgress => "in_progress",
314            Self::Completed => "completed",
315            Self::Failed => "failed",
316        }
317    }
318
319    /// Check if clearance is complete
320    #[must_use]
321    pub fn is_cleared(&self) -> bool {
322        matches!(self, Self::NotRequired | Self::Completed)
323    }
324}
325
326impl std::fmt::Display for ClearanceState {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        write!(f, "{}", self.as_str())
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_withdrawal_state_default() {
338        let state = WithdrawalState::default();
339        assert_eq!(state, WithdrawalState::Unconfirmed);
340    }
341
342    #[test]
343    fn test_withdrawal_state_as_str() {
344        assert_eq!(WithdrawalState::Unconfirmed.as_str(), "unconfirmed");
345        assert_eq!(WithdrawalState::Confirmed.as_str(), "confirmed");
346        assert_eq!(WithdrawalState::Cancelled.as_str(), "cancelled");
347        assert_eq!(WithdrawalState::Completed.as_str(), "completed");
348        assert_eq!(WithdrawalState::Interrupted.as_str(), "interrupted");
349        assert_eq!(WithdrawalState::Rejected.as_str(), "rejected");
350    }
351
352    #[test]
353    fn test_withdrawal_state_is_terminal() {
354        assert!(!WithdrawalState::Unconfirmed.is_terminal());
355        assert!(!WithdrawalState::Confirmed.is_terminal());
356        assert!(WithdrawalState::Cancelled.is_terminal());
357        assert!(WithdrawalState::Completed.is_terminal());
358        assert!(WithdrawalState::Interrupted.is_terminal());
359        assert!(WithdrawalState::Rejected.is_terminal());
360    }
361
362    #[test]
363    fn test_withdrawal_state_is_successful() {
364        assert!(!WithdrawalState::Unconfirmed.is_successful());
365        assert!(!WithdrawalState::Cancelled.is_successful());
366        assert!(WithdrawalState::Completed.is_successful());
367    }
368
369    #[test]
370    fn test_withdrawal_state_serialization() {
371        let state = WithdrawalState::Completed;
372        let json = serde_json::to_string(&state).unwrap();
373        assert_eq!(json, "\"completed\"");
374
375        let deserialized: WithdrawalState = serde_json::from_str(&json).unwrap();
376        assert_eq!(deserialized, WithdrawalState::Completed);
377    }
378
379    #[test]
380    fn test_withdrawal_priority_default() {
381        let priority = WithdrawalPriority::default();
382        assert_eq!(priority, WithdrawalPriority::High);
383    }
384
385    #[test]
386    fn test_withdrawal_priority_as_str() {
387        assert_eq!(WithdrawalPriority::Insane.as_str(), "insane");
388        assert_eq!(WithdrawalPriority::ExtremeHigh.as_str(), "extreme_high");
389        assert_eq!(WithdrawalPriority::VeryHigh.as_str(), "very_high");
390        assert_eq!(WithdrawalPriority::High.as_str(), "high");
391        assert_eq!(WithdrawalPriority::Mid.as_str(), "mid");
392        assert_eq!(WithdrawalPriority::Low.as_str(), "low");
393        assert_eq!(WithdrawalPriority::VeryLow.as_str(), "very_low");
394    }
395
396    #[test]
397    fn test_withdrawal_priority_serialization() {
398        let priority = WithdrawalPriority::VeryHigh;
399        let json = serde_json::to_string(&priority).unwrap();
400        assert_eq!(json, "\"very_high\"");
401
402        let deserialized: WithdrawalPriority = serde_json::from_str(&json).unwrap();
403        assert_eq!(deserialized, WithdrawalPriority::VeryHigh);
404    }
405
406    #[test]
407    fn test_deposit_address_new() {
408        let addr = DepositAddress::new(
409            "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
410            "BTC".to_string(),
411            1640995200000,
412        );
413        assert_eq!(addr.address, "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh");
414        assert_eq!(addr.currency, "BTC");
415        assert_eq!(addr.address_type, AddressType::Deposit);
416        assert_eq!(addr.creation_timestamp, 1640995200000);
417    }
418
419    #[test]
420    fn test_deposit_address_serialization() {
421        let addr = DepositAddress::new(
422            "0x742d35Cc6634C0532925a3b844Bc9e7595f".to_string(),
423            "ETH".to_string(),
424            1640995200000,
425        );
426        let json = serde_json::to_string(&addr).unwrap();
427        let deserialized: DepositAddress = serde_json::from_str(&json).unwrap();
428        assert_eq!(addr, deserialized);
429    }
430
431    #[test]
432    fn test_withdrawal_request_new() {
433        let request = WithdrawalRequest::new(
434            "BTC".to_string(),
435            "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
436            1.5,
437        );
438        assert_eq!(request.currency, "BTC");
439        assert_eq!(request.amount, 1.5);
440        assert!(request.priority.is_none());
441    }
442
443    #[test]
444    fn test_withdrawal_request_with_priority() {
445        let request = WithdrawalRequest::new("BTC".to_string(), "address".to_string(), 1.0)
446            .with_priority(WithdrawalPriority::VeryHigh);
447        assert_eq!(request.priority, Some(WithdrawalPriority::VeryHigh));
448    }
449
450    #[test]
451    fn test_deposit_id_new() {
452        let deposit_id = DepositId::new(
453            "BTC".to_string(),
454            123,
455            "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz".to_string(),
456            "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda".to_string(),
457        );
458        assert_eq!(deposit_id.currency, "BTC");
459        assert_eq!(deposit_id.user_id, 123);
460    }
461
462    #[test]
463    fn test_deposit_id_serialization() {
464        let deposit_id = DepositId::new(
465            "BTC".to_string(),
466            123,
467            "address".to_string(),
468            "tx_hash".to_string(),
469        );
470        let json = serde_json::to_string(&deposit_id).unwrap();
471        let deserialized: DepositId = serde_json::from_str(&json).unwrap();
472        assert_eq!(deposit_id, deserialized);
473    }
474
475    #[test]
476    fn test_clearance_originator_personal() {
477        let originator = ClearanceOriginator::personal("123 Main St".to_string());
478        assert!(originator.is_personal);
479        assert!(originator.is_self_transfer());
480        assert!(!originator.is_individual());
481        assert!(!originator.is_company());
482    }
483
484    #[test]
485    fn test_clearance_originator_individual() {
486        let originator = ClearanceOriginator::individual(
487            "John".to_string(),
488            "Doe".to_string(),
489            "123 Main St".to_string(),
490        );
491        assert!(!originator.is_personal);
492        assert!(!originator.is_self_transfer());
493        assert!(originator.is_individual());
494        assert!(!originator.is_company());
495        assert_eq!(originator.first_name, Some("John".to_string()));
496        assert_eq!(originator.last_name, Some("Doe".to_string()));
497    }
498
499    #[test]
500    fn test_clearance_originator_company() {
501        let originator =
502            ClearanceOriginator::company("Acme Corp".to_string(), "456 Business Ave".to_string());
503        assert!(!originator.is_personal);
504        assert!(!originator.is_self_transfer());
505        assert!(!originator.is_individual());
506        assert!(originator.is_company());
507        assert_eq!(originator.company_name, Some("Acme Corp".to_string()));
508    }
509
510    #[test]
511    fn test_clearance_originator_serialization() {
512        let originator = ClearanceOriginator::individual(
513            "John".to_string(),
514            "Doe".to_string(),
515            "123 Main St".to_string(),
516        );
517        let json = serde_json::to_string(&originator).unwrap();
518        let deserialized: ClearanceOriginator = serde_json::from_str(&json).unwrap();
519        assert_eq!(originator, deserialized);
520    }
521
522    #[test]
523    fn test_clearance_state_default() {
524        let state = ClearanceState::default();
525        assert_eq!(state, ClearanceState::NotRequired);
526    }
527
528    #[test]
529    fn test_clearance_state_as_str() {
530        assert_eq!(ClearanceState::NotRequired.as_str(), "not_required");
531        assert_eq!(ClearanceState::InProgress.as_str(), "in_progress");
532        assert_eq!(ClearanceState::Completed.as_str(), "completed");
533        assert_eq!(ClearanceState::Failed.as_str(), "failed");
534    }
535
536    #[test]
537    fn test_clearance_state_is_cleared() {
538        assert!(ClearanceState::NotRequired.is_cleared());
539        assert!(!ClearanceState::InProgress.is_cleared());
540        assert!(ClearanceState::Completed.is_cleared());
541        assert!(!ClearanceState::Failed.is_cleared());
542    }
543
544    #[test]
545    fn test_clearance_state_serialization() {
546        let state = ClearanceState::InProgress;
547        let json = serde_json::to_string(&state).unwrap();
548        assert_eq!(json, "\"in_progress\"");
549
550        let deserialized: ClearanceState = serde_json::from_str(&json).unwrap();
551        assert_eq!(deserialized, ClearanceState::InProgress);
552    }
553}