Skip to main content

deribit_http/model/
beneficiary.rs

1//! Address beneficiary models for wallet endpoints
2
3use serde::{Deserialize, Serialize};
4
5/// Address beneficiary information returned by save/get/list operations.
6///
7/// Contains all information about a beneficiary associated with
8/// a cryptocurrency address for travel rule compliance.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct AddressBeneficiary {
11    /// Currency symbol (e.g., "BTC", "ETH")
12    pub currency: String,
13    /// Address in proper format for the currency
14    pub address: String,
15    /// User ID associated with this beneficiary
16    #[serde(default)]
17    pub user_id: Option<u64>,
18    /// Whether the user agreed to share information with third parties
19    pub agreed: bool,
20    /// Whether this is a personal wallet owned by the user
21    pub personal: bool,
22    /// Whether the address belongs to an unhosted wallet
23    pub unhosted: bool,
24    /// Name of the beneficiary VASP (Virtual Asset Service Provider)
25    #[serde(default)]
26    pub beneficiary_vasp_name: Option<String>,
27    /// DID (Decentralized Identifier) of the beneficiary VASP
28    #[serde(default)]
29    pub beneficiary_vasp_did: Option<String>,
30    /// Website of the beneficiary VASP
31    #[serde(default)]
32    pub beneficiary_vasp_website: Option<String>,
33    /// First name of the beneficiary (if a person)
34    #[serde(default)]
35    pub beneficiary_first_name: Option<String>,
36    /// Last name of the beneficiary (if a person)
37    #[serde(default)]
38    pub beneficiary_last_name: Option<String>,
39    /// Company name of the beneficiary (if a company)
40    #[serde(default)]
41    pub beneficiary_company_name: Option<String>,
42    /// Geographical address of the beneficiary
43    #[serde(default)]
44    pub beneficiary_address: Option<String>,
45    /// Tag for XRP addresses (optional)
46    #[serde(default)]
47    pub tag: Option<String>,
48    /// Creation timestamp in milliseconds since Unix epoch
49    #[serde(default, alias = "created")]
50    pub creation_timestamp: Option<u64>,
51    /// Update timestamp in milliseconds since Unix epoch
52    #[serde(default, alias = "updated")]
53    pub update_timestamp: Option<u64>,
54}
55
56/// Request parameters for saving an address beneficiary.
57///
58/// All fields required by the API are marked as non-optional.
59/// Optional fields use `Option<T>`.
60#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
61pub struct SaveAddressBeneficiaryRequest {
62    /// Currency symbol (required)
63    pub currency: String,
64    /// Address in currency format (required)
65    pub address: String,
66    /// Tag for XRP addresses (optional)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub tag: Option<String>,
69    /// User agrees to share information with third parties (required)
70    pub agreed: bool,
71    /// Whether this is a personal wallet (required)
72    pub personal: bool,
73    /// Whether the address belongs to an unhosted wallet (required)
74    pub unhosted: bool,
75    /// Name of the beneficiary VASP (required)
76    pub beneficiary_vasp_name: String,
77    /// DID of the beneficiary VASP (required)
78    pub beneficiary_vasp_did: String,
79    /// Website of the beneficiary VASP (optional, required if VASP not in known list)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub beneficiary_vasp_website: Option<String>,
82    /// First name of the beneficiary (optional, for persons)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub beneficiary_first_name: Option<String>,
85    /// Last name of the beneficiary (optional, for persons)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub beneficiary_last_name: Option<String>,
88    /// Company name of the beneficiary (optional, for companies)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub beneficiary_company_name: Option<String>,
91    /// Geographical address of the beneficiary (required)
92    pub beneficiary_address: String,
93}
94
95/// Request parameters for listing address beneficiaries with filtering and pagination.
96#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
97pub struct ListAddressBeneficiariesRequest {
98    /// Filter by currency symbol
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub currency: Option<String>,
101    /// Filter by address
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub address: Option<String>,
104    /// Tag for XRP addresses
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub tag: Option<String>,
107    /// Filter by creation timestamp (before), in milliseconds
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub created_before: Option<u64>,
110    /// Filter by creation timestamp (after), in milliseconds
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub created_after: Option<u64>,
113    /// Filter by update timestamp (before), in milliseconds
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub updated_before: Option<u64>,
116    /// Filter by update timestamp (after), in milliseconds
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub updated_after: Option<u64>,
119    /// Filter by personal wallet flag
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub personal: Option<bool>,
122    /// Filter by unhosted wallet flag
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub unhosted: Option<bool>,
125    /// Filter by beneficiary VASP name
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub beneficiary_vasp_name: Option<String>,
128    /// Filter by beneficiary VASP DID
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub beneficiary_vasp_did: Option<String>,
131    /// Filter by beneficiary VASP website
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub beneficiary_vasp_website: Option<String>,
134    /// Maximum number of results to return
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub limit: Option<u32>,
137    /// Continuation token for pagination
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub continuation: Option<String>,
140}
141
142/// Paginated response for listing address beneficiaries.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct ListAddressBeneficiariesResponse {
145    /// List of address beneficiaries
146    pub data: Vec<AddressBeneficiary>,
147    /// Total count of results available
148    #[serde(default)]
149    pub count: Option<u64>,
150    /// Continuation token for fetching the next page
151    #[serde(default)]
152    pub continuation: Option<String>,
153}
154
155/// Deposit identifier for `set_clearance_originator`.
156///
157/// Identifies a specific deposit transaction.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct DepositId {
160    /// Currency symbol
161    pub currency: String,
162    /// User ID of the (sub)account
163    pub user_id: u64,
164    /// Deposit address in currency format
165    pub address: String,
166    /// Transaction hash in proper format for the currency
167    pub tx_hash: String,
168}
169
170/// Originator information for `set_clearance_originator`.
171///
172/// Contains details about the originator of a deposit for travel rule compliance.
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct Originator {
175    /// Whether the user is the originator of the deposit
176    pub is_personal: bool,
177    /// Company name (if originator is a legal entity)
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub company_name: Option<String>,
180    /// First name (if originator is a person)
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub first_name: Option<String>,
183    /// Last name (if originator is a person)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub last_name: Option<String>,
186    /// Geographical address of the originator
187    pub address: String,
188}
189
190/// Result of `set_clearance_originator` operation.
191///
192/// Contains the deposit information with updated clearance state.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct ClearanceDepositResult {
195    /// Currency symbol
196    pub currency: String,
197    /// User ID
198    #[serde(default)]
199    pub user_id: Option<u64>,
200    /// Deposit address
201    pub address: String,
202    /// Amount of funds in the currency
203    pub amount: f64,
204    /// Deposit state: "pending", "completed", "rejected", "replaced"
205    pub state: String,
206    /// Transaction ID in proper format for the currency
207    #[serde(default)]
208    pub transaction_id: Option<String>,
209    /// Source address of the deposit
210    #[serde(default)]
211    pub source_address: Option<String>,
212    /// Timestamp when deposit was received, in milliseconds
213    #[serde(default)]
214    pub received_timestamp: Option<u64>,
215    /// Timestamp when deposit was last updated, in milliseconds
216    #[serde(default)]
217    pub updated_timestamp: Option<u64>,
218    /// Optional note
219    #[serde(default)]
220    pub note: Option<String>,
221    /// Clearance state: "in_progress", "pending_admin_decision", "pending_user_input",
222    /// "success", "failed", "cancelled", "refund_initiated", "refunded"
223    #[serde(default)]
224    pub clearance_state: Option<String>,
225    /// Refund transaction ID if applicable
226    #[serde(default)]
227    pub refund_transaction_id: Option<String>,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_address_beneficiary_deserialize() {
236        let json = r#"{
237            "currency": "BTC",
238            "address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf0uyj",
239            "user_id": 1016,
240            "agreed": true,
241            "personal": false,
242            "unhosted": false,
243            "beneficiary_vasp_name": "Money's Gone",
244            "beneficiary_vasp_did": "did:example:123456789abcdefghi",
245            "beneficiary_vasp_website": "https://example.com",
246            "beneficiary_first_name": "John",
247            "beneficiary_last_name": "Doe",
248            "beneficiary_company_name": "Example Corp",
249            "beneficiary_address": "NL, Amsterdam, Street, 1",
250            "created": 1536569522277,
251            "updated": 1536569522277
252        }"#;
253
254        let beneficiary: AddressBeneficiary = serde_json::from_str(json).unwrap();
255        assert_eq!(beneficiary.currency, "BTC");
256        assert_eq!(
257            beneficiary.address,
258            "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf0uyj"
259        );
260        assert!(beneficiary.agreed);
261        assert!(!beneficiary.personal);
262        assert!(!beneficiary.unhosted);
263        assert_eq!(
264            beneficiary.beneficiary_vasp_name,
265            Some("Money's Gone".to_string())
266        );
267        assert_eq!(beneficiary.beneficiary_first_name, Some("John".to_string()));
268        assert_eq!(beneficiary.creation_timestamp, Some(1536569522277));
269    }
270
271    #[test]
272    fn test_save_request_serialize() {
273        let request = SaveAddressBeneficiaryRequest {
274            currency: "BTC".to_string(),
275            address: "bc1qtest".to_string(),
276            tag: None,
277            agreed: true,
278            personal: false,
279            unhosted: false,
280            beneficiary_vasp_name: "Test VASP".to_string(),
281            beneficiary_vasp_did: "did:test:123".to_string(),
282            beneficiary_vasp_website: Some("https://test.com".to_string()),
283            beneficiary_first_name: Some("John".to_string()),
284            beneficiary_last_name: Some("Doe".to_string()),
285            beneficiary_company_name: None,
286            beneficiary_address: "Test Address".to_string(),
287        };
288
289        let json = serde_json::to_string(&request).unwrap();
290        assert!(json.contains("\"currency\":\"BTC\""));
291        assert!(json.contains("\"agreed\":true"));
292        assert!(!json.contains("\"tag\"")); // Should be skipped when None
293        assert!(!json.contains("beneficiary_company_name")); // Should be skipped when None
294    }
295
296    #[test]
297    fn test_list_response_deserialize() {
298        let json = r#"{
299            "data": [
300                {
301                    "currency": "BTC",
302                    "address": "bc1qtest",
303                    "agreed": true,
304                    "personal": false,
305                    "unhosted": false
306                }
307            ],
308            "continuation": "xY7T6cutS3t2B9YtaDkE6TS379oKnkzTvmEDUnEUP2Msa9xKWNNaT",
309            "count": 1
310        }"#;
311
312        let response: ListAddressBeneficiariesResponse = serde_json::from_str(json).unwrap();
313        assert_eq!(response.data.len(), 1);
314        assert_eq!(response.count, Some(1));
315        assert!(response.continuation.is_some());
316    }
317
318    #[test]
319    fn test_clearance_deposit_result_deserialize() {
320        let json = r#"{
321            "currency": "BTC",
322            "user_id": 123,
323            "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz",
324            "amount": 0.4,
325            "state": "completed",
326            "transaction_id": "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda",
327            "source_address": "A3BqqD5GRJ8wHy1PYyCXTe9ke5226Fha123",
328            "received_timestamp": 1550574558607,
329            "updated_timestamp": 1550574558807,
330            "note": "Note",
331            "clearance_state": "in_progress"
332        }"#;
333
334        let result: ClearanceDepositResult = serde_json::from_str(json).unwrap();
335        assert_eq!(result.currency, "BTC");
336        assert_eq!(result.user_id, Some(123));
337        let amount_diff = (result.amount - 0.4).abs();
338        assert!(amount_diff < f64::EPSILON);
339        assert_eq!(result.state, "completed");
340        assert_eq!(result.clearance_state, Some("in_progress".to_string()));
341    }
342
343    #[test]
344    fn test_deposit_id_serialize() {
345        let deposit_id = DepositId {
346            currency: "BTC".to_string(),
347            user_id: 123,
348            address: "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz".to_string(),
349            tx_hash: "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda".to_string(),
350        };
351
352        let json = serde_json::to_string(&deposit_id).unwrap();
353        assert!(json.contains("\"currency\":\"BTC\""));
354        assert!(json.contains("\"user_id\":123"));
355        assert!(json.contains("\"tx_hash\":"));
356    }
357
358    #[test]
359    fn test_originator_serialize() {
360        let originator = Originator {
361            is_personal: false,
362            company_name: Some("Company Name".to_string()),
363            first_name: Some("First".to_string()),
364            last_name: Some("Last".to_string()),
365            address: "NL, Amsterdam, Street, 1".to_string(),
366        };
367
368        let json = serde_json::to_string(&originator).unwrap();
369        assert!(json.contains("\"is_personal\":false"));
370        assert!(json.contains("\"company_name\":\"Company Name\""));
371        assert!(json.contains("\"address\":\"NL, Amsterdam, Street, 1\""));
372    }
373
374    #[test]
375    fn test_list_request_default() {
376        let request = ListAddressBeneficiariesRequest::default();
377        assert!(request.currency.is_none());
378        assert!(request.limit.is_none());
379        assert!(request.continuation.is_none());
380    }
381}