Skip to main content

rust_okx/api/
funding.rs

1//! Authenticated funding-account and asset endpoints (`/api/v5/asset/*`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::NumberString;
8use crate::transport::Transport;
9
10const NON_TRADABLE_ASSETS: &str = "/api/v5/asset/non-tradable-assets";
11const DEPOSIT_ADDRESS: &str = "/api/v5/asset/deposit-address";
12const BALANCES: &str = "/api/v5/asset/balances";
13const TRANSFER: &str = "/api/v5/asset/transfer";
14const TRANSFER_STATE: &str = "/api/v5/asset/transfer-state";
15const WITHDRAWAL: &str = "/api/v5/asset/withdrawal";
16const DEPOSIT_HISTORY: &str = "/api/v5/asset/deposit-history";
17const CURRENCIES: &str = "/api/v5/asset/currencies";
18const PURCHASE_REDEMPT: &str = "/api/v5/asset/purchase_redempt";
19const BILLS: &str = "/api/v5/asset/bills";
20const DEPOSIT_LIGHTNING: &str = "/api/v5/asset/deposit-lightning";
21const WITHDRAWAL_LIGHTNING: &str = "/api/v5/asset/withdrawal-lightning";
22const CANCEL_WITHDRAWAL: &str = "/api/v5/asset/cancel-withdrawal";
23const CONVERT_DUST_ASSETS: &str = "/api/v5/asset/convert-dust-assets";
24const ASSET_VALUATION: &str = "/api/v5/asset/asset-valuation";
25const WITHDRAWAL_HISTORY: &str = "/api/v5/asset/withdrawal-history";
26const DEPOSIT_WITHDRAW_STATUS: &str = "/api/v5/asset/deposit-withdraw-status";
27
28/// Accessor for authenticated funding-account and asset endpoints.
29///
30/// Obtain one via [`OkxClient::funding`](crate::OkxClient::funding). All methods
31/// require credentials. Mutating endpoints operate on the funding account, not
32/// the trading account.
33pub struct Funding<'a, T> {
34    client: &'a OkxClient<T>,
35}
36
37impl<'a, T: Transport> Funding<'a, T> {
38    pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
39        Self { client }
40    }
41
42    /// Retrieve currency metadata and chain settings.
43    ///
44    /// `GET /api/v5/asset/currencies`. Authenticated.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`Error::Configuration`] without credentials, [`Error::Api`] on a
49    /// non-zero OKX code, or transport/decode errors.
50    pub async fn get_currencies(&self, ccy: Option<&str>) -> Result<Vec<Currency>, Error> {
51        let query = CcyQuery { ccy };
52        self.client.get(CURRENCIES, &query, true).await
53    }
54
55    /// Retrieve funding-account balances.
56    ///
57    /// `GET /api/v5/asset/balances`. Authenticated.
58    ///
59    /// # Errors
60    ///
61    /// See [`get_currencies`](Self::get_currencies).
62    pub async fn get_balances(&self, ccy: Option<&str>) -> Result<Vec<FundingBalance>, Error> {
63        let query = CcyQuery { ccy };
64        self.client.get(BALANCES, &query, true).await
65    }
66
67    /// Retrieve non-tradable assets.
68    ///
69    /// `GET /api/v5/asset/non-tradable-assets`. Authenticated.
70    ///
71    /// # Errors
72    ///
73    /// See [`get_currencies`](Self::get_currencies).
74    pub async fn get_non_tradable_assets(
75        &self,
76        ccy: Option<&str>,
77    ) -> Result<Vec<NonTradableAsset>, Error> {
78        let query = CcyQuery { ccy };
79        self.client.get(NON_TRADABLE_ASSETS, &query, true).await
80    }
81
82    /// Retrieve deposit addresses for a currency.
83    ///
84    /// `GET /api/v5/asset/deposit-address`. Authenticated.
85    ///
86    /// # Errors
87    ///
88    /// See [`get_currencies`](Self::get_currencies).
89    pub async fn get_deposit_address(&self, ccy: &str) -> Result<Vec<DepositAddress>, Error> {
90        let query = RequiredCcyQuery { ccy };
91        self.client.get(DEPOSIT_ADDRESS, &query, true).await
92    }
93
94    /// Transfer funds between OKX account types.
95    ///
96    /// `POST /api/v5/asset/transfer`. Authenticated.
97    ///
98    /// # Errors
99    ///
100    /// See [`get_currencies`](Self::get_currencies).
101    pub async fn funds_transfer(
102        &self,
103        request: &FundsTransferRequest,
104    ) -> Result<Vec<TransferResult>, Error> {
105        self.client.post(TRANSFER, request, true).await
106    }
107
108    /// Retrieve the state of a funds transfer.
109    ///
110    /// `GET /api/v5/asset/transfer-state`. Authenticated.
111    ///
112    /// # Errors
113    ///
114    /// See [`get_currencies`](Self::get_currencies).
115    pub async fn transfer_state(
116        &self,
117        trans_id: &str,
118        transfer_type: Option<&str>,
119    ) -> Result<Vec<TransferState>, Error> {
120        let query = TransferStateQuery {
121            trans_id,
122            transfer_type,
123        };
124        self.client.get(TRANSFER_STATE, &query, true).await
125    }
126
127    /// Withdraw funds from OKX.
128    ///
129    /// `POST /api/v5/asset/withdrawal`. Authenticated. This is a real asset
130    /// movement endpoint; build requests deliberately with
131    /// [`WithdrawalRequest::new`].
132    ///
133    /// # Errors
134    ///
135    /// See [`get_currencies`](Self::get_currencies).
136    pub async fn withdrawal(
137        &self,
138        request: &WithdrawalRequest,
139    ) -> Result<Vec<WithdrawalResult>, Error> {
140        self.client.post(WITHDRAWAL, request, true).await
141    }
142
143    /// Retrieve deposit history.
144    ///
145    /// `GET /api/v5/asset/deposit-history`. Authenticated.
146    ///
147    /// # Errors
148    ///
149    /// See [`get_currencies`](Self::get_currencies).
150    pub async fn get_deposit_history(
151        &self,
152        request: &DepositHistoryRequest,
153    ) -> Result<Vec<DepositRecord>, Error> {
154        self.client.get(DEPOSIT_HISTORY, request, true).await
155    }
156
157    /// Subscribe or redeem savings through the legacy purchase/redemption
158    /// endpoint.
159    ///
160    /// `POST /api/v5/asset/purchase_redempt`. Authenticated.
161    ///
162    /// # Errors
163    ///
164    /// See [`get_currencies`](Self::get_currencies).
165    pub async fn purchase_redempt(
166        &self,
167        ccy: &str,
168        amt: &str,
169        side: &str,
170        rate: &str,
171    ) -> Result<Vec<PurchaseRedemptResult>, Error> {
172        let body = PurchaseRedemptBody {
173            ccy,
174            amt,
175            side,
176            rate,
177        };
178        self.client.post(PURCHASE_REDEMPT, &body, true).await
179    }
180
181    /// Retrieve funding-account bills.
182    ///
183    /// `GET /api/v5/asset/bills`. Authenticated.
184    ///
185    /// # Errors
186    ///
187    /// See [`get_currencies`](Self::get_currencies).
188    pub async fn get_bills(
189        &self,
190        request: &FundingBillsRequest,
191    ) -> Result<Vec<FundingBill>, Error> {
192        self.client.get(BILLS, request, true).await
193    }
194
195    /// Create or retrieve a Lightning Network deposit invoice.
196    ///
197    /// `GET /api/v5/asset/deposit-lightning`. Authenticated.
198    ///
199    /// # Errors
200    ///
201    /// See [`get_currencies`](Self::get_currencies).
202    pub async fn get_deposit_lightning(
203        &self,
204        request: &DepositLightningRequest,
205    ) -> Result<Vec<DepositLightning>, Error> {
206        self.client.get(DEPOSIT_LIGHTNING, request, true).await
207    }
208
209    /// Withdraw through the Lightning Network.
210    ///
211    /// `POST /api/v5/asset/withdrawal-lightning`. Authenticated.
212    ///
213    /// # Errors
214    ///
215    /// See [`get_currencies`](Self::get_currencies).
216    pub async fn withdrawal_lightning(
217        &self,
218        request: &WithdrawalLightningRequest,
219    ) -> Result<Vec<WithdrawalLightning>, Error> {
220        self.client.post(WITHDRAWAL_LIGHTNING, request, true).await
221    }
222
223    /// Cancel a withdrawal.
224    ///
225    /// `POST /api/v5/asset/cancel-withdrawal`. Authenticated.
226    ///
227    /// # Errors
228    ///
229    /// See [`get_currencies`](Self::get_currencies).
230    pub async fn cancel_withdrawal(&self, wd_id: &str) -> Result<Vec<WithdrawalResult>, Error> {
231        let body = WithdrawalIdBody { wd_id };
232        self.client.post(CANCEL_WITHDRAWAL, &body, true).await
233    }
234
235    /// Convert small balances into another asset.
236    ///
237    /// `POST /api/v5/asset/convert-dust-assets`. Authenticated.
238    ///
239    /// # Errors
240    ///
241    /// See [`get_currencies`](Self::get_currencies).
242    pub async fn convert_dust_assets(
243        &self,
244        ccy: &[&str],
245    ) -> Result<Vec<ConvertDustAssetsResult>, Error> {
246        let body = ConvertDustAssetsBody { ccy };
247        self.client.post(CONVERT_DUST_ASSETS, &body, true).await
248    }
249
250    /// Retrieve total asset valuation.
251    ///
252    /// `GET /api/v5/asset/asset-valuation`. Authenticated.
253    ///
254    /// # Errors
255    ///
256    /// See [`get_currencies`](Self::get_currencies).
257    pub async fn get_asset_valuation(
258        &self,
259        ccy: Option<&str>,
260    ) -> Result<Vec<AssetValuation>, Error> {
261        let query = CcyQuery { ccy };
262        self.client.get(ASSET_VALUATION, &query, true).await
263    }
264
265    /// Retrieve deposit/withdrawal status.
266    ///
267    /// `GET /api/v5/asset/deposit-withdraw-status`. Authenticated.
268    ///
269    /// # Errors
270    ///
271    /// See [`get_currencies`](Self::get_currencies).
272    pub async fn get_deposit_withdraw_status(
273        &self,
274        request: &DepositWithdrawStatusRequest,
275    ) -> Result<Vec<DepositWithdrawStatus>, Error> {
276        self.client
277            .get(DEPOSIT_WITHDRAW_STATUS, request, true)
278            .await
279    }
280
281    /// Retrieve withdrawal history.
282    ///
283    /// `GET /api/v5/asset/withdrawal-history`. Authenticated.
284    ///
285    /// # Errors
286    ///
287    /// See [`get_currencies`](Self::get_currencies).
288    pub async fn get_withdrawal_history(
289        &self,
290        request: &WithdrawalHistoryRequest,
291    ) -> Result<Vec<WithdrawalRecord>, Error> {
292        self.client.get(WITHDRAWAL_HISTORY, request, true).await
293    }
294}
295
296#[derive(Debug, Serialize)]
297struct CcyQuery<'a> {
298    #[serde(skip_serializing_if = "Option::is_none")]
299    ccy: Option<&'a str>,
300}
301
302#[derive(Debug, Serialize)]
303struct RequiredCcyQuery<'a> {
304    ccy: &'a str,
305}
306
307#[derive(Debug, Serialize)]
308#[serde(rename_all = "camelCase")]
309struct TransferStateQuery<'a> {
310    #[serde(rename = "transId")]
311    trans_id: &'a str,
312    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
313    transfer_type: Option<&'a str>,
314}
315
316#[derive(Debug, Serialize)]
317struct PurchaseRedemptBody<'a> {
318    ccy: &'a str,
319    amt: &'a str,
320    side: &'a str,
321    rate: &'a str,
322}
323
324#[derive(Debug, Serialize)]
325#[serde(rename_all = "camelCase")]
326struct WithdrawalIdBody<'a> {
327    #[serde(rename = "wdId")]
328    wd_id: &'a str,
329}
330
331#[derive(Debug, Serialize)]
332struct ConvertDustAssetsBody<'a> {
333    ccy: &'a [&'a str],
334}
335
336/// Request body for [`Funding::funds_transfer`].
337#[derive(Debug, Clone, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct FundsTransferRequest {
340    ccy: String,
341    amt: String,
342    #[serde(rename = "from")]
343    from_account: String,
344    to: String,
345    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
346    transfer_type: Option<String>,
347    #[serde(rename = "subAcct", skip_serializing_if = "Option::is_none")]
348    sub_account: Option<String>,
349    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
350    inst_id: Option<String>,
351    #[serde(rename = "toInstId", skip_serializing_if = "Option::is_none")]
352    to_inst_id: Option<String>,
353    #[serde(rename = "loanTrans", skip_serializing_if = "Option::is_none")]
354    loan_transfer: Option<String>,
355}
356
357impl FundsTransferRequest {
358    /// Create a funds-transfer request.
359    pub fn new(
360        ccy: impl Into<String>,
361        amt: impl Into<String>,
362        from_account: impl Into<String>,
363        to: impl Into<String>,
364    ) -> Self {
365        Self {
366            ccy: ccy.into(),
367            amt: amt.into(),
368            from_account: from_account.into(),
369            to: to.into(),
370            transfer_type: None,
371            sub_account: None,
372            inst_id: None,
373            to_inst_id: None,
374            loan_transfer: None,
375        }
376    }
377
378    /// Set transfer type, e.g. `0` for transfer within account.
379    pub fn transfer_type(mut self, transfer_type: impl Into<String>) -> Self {
380        self.transfer_type = Some(transfer_type.into());
381        self
382    }
383
384    /// Set sub-account name.
385    pub fn sub_account(mut self, sub_account: impl Into<String>) -> Self {
386        self.sub_account = Some(sub_account.into());
387        self
388    }
389
390    /// Set source instrument ID.
391    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
392        self.inst_id = Some(inst_id.into());
393        self
394    }
395
396    /// Set destination instrument ID.
397    pub fn to_inst_id(mut self, to_inst_id: impl Into<String>) -> Self {
398        self.to_inst_id = Some(to_inst_id.into());
399        self
400    }
401
402    /// Set whether this is a loan transfer.
403    pub fn loan_transfer(mut self, loan_transfer: impl Into<String>) -> Self {
404        self.loan_transfer = Some(loan_transfer.into());
405        self
406    }
407}
408
409/// Request body for [`Funding::withdrawal`].
410#[derive(Debug, Clone, Serialize)]
411#[serde(rename_all = "camelCase")]
412pub struct WithdrawalRequest {
413    ccy: String,
414    amt: String,
415    dest: String,
416    #[serde(rename = "toAddr")]
417    to_addr: String,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    chain: Option<String>,
420    #[serde(rename = "areaCode", skip_serializing_if = "Option::is_none")]
421    area_code: Option<String>,
422    #[serde(rename = "clientId", skip_serializing_if = "Option::is_none")]
423    client_id: Option<String>,
424    #[serde(rename = "toAddrType", skip_serializing_if = "Option::is_none")]
425    to_addr_type: Option<String>,
426}
427
428impl WithdrawalRequest {
429    /// Create a withdrawal request.
430    pub fn new(
431        ccy: impl Into<String>,
432        amt: impl Into<String>,
433        dest: impl Into<String>,
434        to_addr: impl Into<String>,
435    ) -> Self {
436        Self {
437            ccy: ccy.into(),
438            amt: amt.into(),
439            dest: dest.into(),
440            to_addr: to_addr.into(),
441            chain: None,
442            area_code: None,
443            client_id: None,
444            to_addr_type: None,
445        }
446    }
447
448    /// Set withdrawal chain.
449    pub fn chain(mut self, chain: impl Into<String>) -> Self {
450        self.chain = Some(chain.into());
451        self
452    }
453
454    /// Set phone area code for internal withdrawals.
455    pub fn area_code(mut self, area_code: impl Into<String>) -> Self {
456        self.area_code = Some(area_code.into());
457        self
458    }
459
460    /// Set client withdrawal ID.
461    pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
462        self.client_id = Some(client_id.into());
463        self
464    }
465
466    /// Set address type.
467    pub fn to_addr_type(mut self, to_addr_type: impl Into<String>) -> Self {
468        self.to_addr_type = Some(to_addr_type.into());
469        self
470    }
471}
472
473/// Query parameters for [`Funding::get_deposit_history`].
474#[derive(Debug, Clone, Default, Serialize)]
475#[serde(rename_all = "camelCase")]
476pub struct DepositHistoryRequest {
477    #[serde(skip_serializing_if = "Option::is_none")]
478    ccy: Option<String>,
479    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
480    deposit_type: Option<String>,
481    #[serde(skip_serializing_if = "Option::is_none")]
482    state: Option<String>,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    after: Option<String>,
485    #[serde(skip_serializing_if = "Option::is_none")]
486    before: Option<String>,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    limit: Option<u32>,
489    #[serde(rename = "txId", skip_serializing_if = "Option::is_none")]
490    tx_id: Option<String>,
491    #[serde(rename = "depId", skip_serializing_if = "Option::is_none")]
492    dep_id: Option<String>,
493    #[serde(rename = "fromWdId", skip_serializing_if = "Option::is_none")]
494    from_wd_id: Option<String>,
495}
496
497impl DepositHistoryRequest {
498    /// Create an empty deposit-history query.
499    pub fn new() -> Self {
500        Self::default()
501    }
502
503    /// Filter by currency.
504    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
505        self.ccy = Some(ccy.into());
506        self
507    }
508
509    /// Filter by deposit type.
510    pub fn deposit_type(mut self, deposit_type: impl Into<String>) -> Self {
511        self.deposit_type = Some(deposit_type.into());
512        self
513    }
514
515    /// Filter by state.
516    pub fn state(mut self, state: impl Into<String>) -> Self {
517        self.state = Some(state.into());
518        self
519    }
520
521    /// Page after the given ID.
522    pub fn after(mut self, after: impl Into<String>) -> Self {
523        self.after = Some(after.into());
524        self
525    }
526
527    /// Page before the given ID.
528    pub fn before(mut self, before: impl Into<String>) -> Self {
529        self.before = Some(before.into());
530        self
531    }
532
533    /// Set result limit.
534    pub fn limit(mut self, limit: u32) -> Self {
535        self.limit = Some(limit);
536        self
537    }
538
539    /// Filter by transaction ID.
540    pub fn tx_id(mut self, tx_id: impl Into<String>) -> Self {
541        self.tx_id = Some(tx_id.into());
542        self
543    }
544
545    /// Filter by deposit ID.
546    pub fn dep_id(mut self, dep_id: impl Into<String>) -> Self {
547        self.dep_id = Some(dep_id.into());
548        self
549    }
550
551    /// Filter by source withdrawal ID.
552    pub fn from_wd_id(mut self, from_wd_id: impl Into<String>) -> Self {
553        self.from_wd_id = Some(from_wd_id.into());
554        self
555    }
556}
557
558/// Query parameters for [`Funding::get_withdrawal_history`].
559#[derive(Debug, Clone, Default, Serialize)]
560#[serde(rename_all = "camelCase")]
561pub struct WithdrawalHistoryRequest {
562    #[serde(skip_serializing_if = "Option::is_none")]
563    ccy: Option<String>,
564    #[serde(rename = "wdId", skip_serializing_if = "Option::is_none")]
565    wd_id: Option<String>,
566    #[serde(rename = "clientId", skip_serializing_if = "Option::is_none")]
567    client_id: Option<String>,
568    #[serde(rename = "txId", skip_serializing_if = "Option::is_none")]
569    tx_id: Option<String>,
570    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
571    withdrawal_type: Option<String>,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    state: Option<String>,
574    #[serde(skip_serializing_if = "Option::is_none")]
575    after: Option<String>,
576    #[serde(skip_serializing_if = "Option::is_none")]
577    before: Option<String>,
578    #[serde(skip_serializing_if = "Option::is_none")]
579    limit: Option<u32>,
580    #[serde(rename = "toAddrType", skip_serializing_if = "Option::is_none")]
581    to_addr_type: Option<String>,
582}
583
584impl WithdrawalHistoryRequest {
585    /// Create an empty withdrawal-history query.
586    pub fn new() -> Self {
587        Self::default()
588    }
589
590    /// Filter by currency.
591    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
592        self.ccy = Some(ccy.into());
593        self
594    }
595
596    /// Filter by withdrawal ID.
597    pub fn withdrawal_id(mut self, wd_id: impl Into<String>) -> Self {
598        self.wd_id = Some(wd_id.into());
599        self
600    }
601
602    /// Filter by client ID.
603    pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
604        self.client_id = Some(client_id.into());
605        self
606    }
607
608    /// Filter by transaction ID.
609    pub fn tx_id(mut self, tx_id: impl Into<String>) -> Self {
610        self.tx_id = Some(tx_id.into());
611        self
612    }
613
614    /// Filter by withdrawal type.
615    pub fn withdrawal_type(mut self, withdrawal_type: impl Into<String>) -> Self {
616        self.withdrawal_type = Some(withdrawal_type.into());
617        self
618    }
619
620    /// Filter by state.
621    pub fn state(mut self, state: impl Into<String>) -> Self {
622        self.state = Some(state.into());
623        self
624    }
625
626    /// Page after the given ID.
627    pub fn after(mut self, after: impl Into<String>) -> Self {
628        self.after = Some(after.into());
629        self
630    }
631
632    /// Page before the given ID.
633    pub fn before(mut self, before: impl Into<String>) -> Self {
634        self.before = Some(before.into());
635        self
636    }
637
638    /// Set result limit.
639    pub fn limit(mut self, limit: u32) -> Self {
640        self.limit = Some(limit);
641        self
642    }
643
644    /// Filter by destination address type.
645    pub fn to_addr_type(mut self, to_addr_type: impl Into<String>) -> Self {
646        self.to_addr_type = Some(to_addr_type.into());
647        self
648    }
649}
650
651/// Query parameters for [`Funding::get_bills`].
652#[derive(Debug, Clone, Default, Serialize)]
653pub struct FundingBillsRequest {
654    #[serde(skip_serializing_if = "Option::is_none")]
655    ccy: Option<String>,
656    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
657    bill_type: Option<String>,
658    #[serde(skip_serializing_if = "Option::is_none")]
659    after: Option<String>,
660    #[serde(skip_serializing_if = "Option::is_none")]
661    before: Option<String>,
662    #[serde(skip_serializing_if = "Option::is_none")]
663    limit: Option<u32>,
664}
665
666impl FundingBillsRequest {
667    /// Create an empty funding-bills query.
668    pub fn new() -> Self {
669        Self::default()
670    }
671
672    /// Filter by currency.
673    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
674        self.ccy = Some(ccy.into());
675        self
676    }
677
678    /// Filter by bill type.
679    pub fn bill_type(mut self, bill_type: impl Into<String>) -> Self {
680        self.bill_type = Some(bill_type.into());
681        self
682    }
683
684    /// Page after the given bill ID.
685    pub fn after(mut self, after: impl Into<String>) -> Self {
686        self.after = Some(after.into());
687        self
688    }
689
690    /// Page before the given bill ID.
691    pub fn before(mut self, before: impl Into<String>) -> Self {
692        self.before = Some(before.into());
693        self
694    }
695
696    /// Set result limit.
697    pub fn limit(mut self, limit: u32) -> Self {
698        self.limit = Some(limit);
699        self
700    }
701}
702
703/// Query parameters for [`Funding::get_deposit_lightning`].
704#[derive(Debug, Clone, Serialize)]
705pub struct DepositLightningRequest {
706    ccy: String,
707    amt: String,
708    #[serde(skip_serializing_if = "Option::is_none")]
709    to: Option<String>,
710}
711
712impl DepositLightningRequest {
713    /// Create a Lightning deposit request.
714    pub fn new(ccy: impl Into<String>, amt: impl Into<String>) -> Self {
715        Self {
716            ccy: ccy.into(),
717            amt: amt.into(),
718            to: None,
719        }
720    }
721
722    /// Set recipient.
723    pub fn to(mut self, to: impl Into<String>) -> Self {
724        self.to = Some(to.into());
725        self
726    }
727}
728
729/// Request body for [`Funding::withdrawal_lightning`].
730#[derive(Debug, Clone, Serialize)]
731pub struct WithdrawalLightningRequest {
732    ccy: String,
733    invoice: String,
734    #[serde(skip_serializing_if = "Option::is_none")]
735    memo: Option<String>,
736}
737
738impl WithdrawalLightningRequest {
739    /// Create a Lightning withdrawal request.
740    pub fn new(ccy: impl Into<String>, invoice: impl Into<String>) -> Self {
741        Self {
742            ccy: ccy.into(),
743            invoice: invoice.into(),
744            memo: None,
745        }
746    }
747
748    /// Set withdrawal memo.
749    pub fn memo(mut self, memo: impl Into<String>) -> Self {
750        self.memo = Some(memo.into());
751        self
752    }
753}
754
755/// Query parameters for [`Funding::get_deposit_withdraw_status`].
756#[derive(Debug, Clone, Default, Serialize)]
757#[serde(rename_all = "camelCase")]
758pub struct DepositWithdrawStatusRequest {
759    #[serde(rename = "wdId", skip_serializing_if = "Option::is_none")]
760    wd_id: Option<String>,
761    #[serde(rename = "txId", skip_serializing_if = "Option::is_none")]
762    tx_id: Option<String>,
763    #[serde(skip_serializing_if = "Option::is_none")]
764    ccy: Option<String>,
765    #[serde(skip_serializing_if = "Option::is_none")]
766    to: Option<String>,
767    #[serde(skip_serializing_if = "Option::is_none")]
768    chain: Option<String>,
769}
770
771impl DepositWithdrawStatusRequest {
772    /// Create an empty deposit/withdrawal-status query.
773    pub fn new() -> Self {
774        Self::default()
775    }
776
777    /// Filter by withdrawal ID.
778    pub fn withdrawal_id(mut self, wd_id: impl Into<String>) -> Self {
779        self.wd_id = Some(wd_id.into());
780        self
781    }
782
783    /// Filter by transaction ID.
784    pub fn tx_id(mut self, tx_id: impl Into<String>) -> Self {
785        self.tx_id = Some(tx_id.into());
786        self
787    }
788
789    /// Filter by currency.
790    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
791        self.ccy = Some(ccy.into());
792        self
793    }
794
795    /// Filter by destination address.
796    pub fn to(mut self, to: impl Into<String>) -> Self {
797        self.to = Some(to.into());
798        self
799    }
800
801    /// Filter by chain.
802    pub fn chain(mut self, chain: impl Into<String>) -> Self {
803        self.chain = Some(chain.into());
804        self
805    }
806}
807
808/// Currency metadata and chain settings.
809#[derive(Debug, Clone, Default, Deserialize)]
810#[serde(rename_all = "camelCase")]
811#[non_exhaustive]
812pub struct Currency {
813    /// Currency code.
814    #[serde(default)]
815    pub ccy: String,
816    /// Display name.
817    #[serde(default)]
818    pub name: String,
819    /// Chain name.
820    #[serde(default)]
821    pub chain: String,
822    /// Minimum withdrawal amount.
823    #[serde(default, rename = "minWd")]
824    pub min_wd: NumberString,
825    /// Minimum deposit amount.
826    #[serde(default, rename = "minDep")]
827    pub min_dep: NumberString,
828    /// Withdrawal fee.
829    #[serde(default, rename = "minFee")]
830    pub min_fee: NumberString,
831    /// Whether deposit is enabled.
832    #[serde(default, rename = "canDep")]
833    pub can_dep: bool,
834    /// Whether withdrawal is enabled.
835    #[serde(default, rename = "canWd")]
836    pub can_wd: bool,
837    /// Whether internal transfer is enabled.
838    #[serde(default, rename = "canInternal")]
839    pub can_internal: bool,
840}
841
842/// Funding-account balance for one currency.
843#[derive(Debug, Clone, Default, Deserialize)]
844#[serde(rename_all = "camelCase")]
845#[non_exhaustive]
846pub struct FundingBalance {
847    /// Currency code.
848    #[serde(default)]
849    pub ccy: String,
850    /// Total balance.
851    #[serde(default)]
852    pub bal: NumberString,
853    /// Frozen balance.
854    #[serde(default)]
855    pub frozen_bal: NumberString,
856    /// Available balance.
857    #[serde(default)]
858    pub avail_bal: NumberString,
859}
860
861/// Non-tradable asset row.
862#[derive(Debug, Clone, Default, Deserialize)]
863#[serde(rename_all = "camelCase")]
864#[non_exhaustive]
865pub struct NonTradableAsset {
866    /// Currency code.
867    #[serde(default)]
868    pub ccy: String,
869    /// Asset amount.
870    #[serde(default)]
871    pub amt: NumberString,
872    /// Asset type.
873    #[serde(default, rename = "type")]
874    pub asset_type: String,
875}
876
877/// Deposit address for one currency/chain.
878#[derive(Debug, Clone, Default, Deserialize)]
879#[serde(rename_all = "camelCase")]
880#[non_exhaustive]
881pub struct DepositAddress {
882    /// Currency code.
883    #[serde(default)]
884    pub ccy: String,
885    /// Chain name.
886    #[serde(default)]
887    pub chain: String,
888    /// Deposit address.
889    #[serde(default)]
890    pub addr: String,
891    /// Address tag, memo, or payment ID when required by the chain.
892    #[serde(default)]
893    pub tag: String,
894    /// Selected account.
895    #[serde(default)]
896    pub selected: bool,
897}
898
899/// Funds-transfer result.
900#[derive(Debug, Clone, Default, Deserialize)]
901#[serde(rename_all = "camelCase")]
902#[non_exhaustive]
903pub struct TransferResult {
904    /// Transfer ID.
905    #[serde(default, rename = "transId")]
906    pub trans_id: String,
907    /// Currency code.
908    #[serde(default)]
909    pub ccy: String,
910    /// Transfer amount.
911    #[serde(default)]
912    pub amt: NumberString,
913}
914
915/// Funds-transfer state.
916#[derive(Debug, Clone, Default, Deserialize)]
917#[serde(rename_all = "camelCase")]
918#[non_exhaustive]
919pub struct TransferState {
920    /// Transfer ID.
921    #[serde(default, rename = "transId")]
922    pub trans_id: String,
923    /// Transfer state.
924    #[serde(default)]
925    pub state: String,
926    /// Currency code.
927    #[serde(default)]
928    pub ccy: String,
929    /// Transfer amount.
930    #[serde(default)]
931    pub amt: NumberString,
932    /// Source account.
933    #[serde(default, rename = "from")]
934    pub from_account: String,
935    /// Destination account.
936    #[serde(default)]
937    pub to: String,
938}
939
940/// Withdrawal result.
941#[derive(Debug, Clone, Default, Deserialize)]
942#[serde(rename_all = "camelCase")]
943#[non_exhaustive]
944pub struct WithdrawalResult {
945    /// Withdrawal ID.
946    #[serde(default, rename = "wdId")]
947    pub wd_id: String,
948    /// Client withdrawal ID.
949    #[serde(default, rename = "clientId")]
950    pub client_id: String,
951    /// Currency code.
952    #[serde(default)]
953    pub ccy: String,
954    /// Withdrawal amount.
955    #[serde(default)]
956    pub amt: NumberString,
957}
958
959/// Deposit history row.
960#[derive(Debug, Clone, Default, Deserialize)]
961#[serde(rename_all = "camelCase")]
962#[non_exhaustive]
963pub struct DepositRecord {
964    /// Deposit ID.
965    #[serde(default, rename = "depId")]
966    pub dep_id: String,
967    /// Transaction ID.
968    #[serde(default, rename = "txId")]
969    pub tx_id: String,
970    /// Currency code.
971    #[serde(default)]
972    pub ccy: String,
973    /// Chain name.
974    #[serde(default)]
975    pub chain: String,
976    /// Deposit amount.
977    #[serde(default)]
978    pub amt: NumberString,
979    /// Deposit state.
980    #[serde(default)]
981    pub state: String,
982    /// Timestamp.
983    #[serde(default)]
984    pub ts: NumberString,
985}
986
987/// Withdrawal history row.
988#[derive(Debug, Clone, Default, Deserialize)]
989#[serde(rename_all = "camelCase")]
990#[non_exhaustive]
991pub struct WithdrawalRecord {
992    /// Withdrawal ID.
993    #[serde(default, rename = "wdId")]
994    pub wd_id: String,
995    /// Client withdrawal ID.
996    #[serde(default, rename = "clientId")]
997    pub client_id: String,
998    /// Transaction ID.
999    #[serde(default, rename = "txId")]
1000    pub tx_id: String,
1001    /// Currency code.
1002    #[serde(default)]
1003    pub ccy: String,
1004    /// Chain name.
1005    #[serde(default)]
1006    pub chain: String,
1007    /// Withdrawal amount.
1008    #[serde(default)]
1009    pub amt: NumberString,
1010    /// Fee.
1011    #[serde(default)]
1012    pub fee: NumberString,
1013    /// Withdrawal state.
1014    #[serde(default)]
1015    pub state: String,
1016    /// Timestamp.
1017    #[serde(default)]
1018    pub ts: NumberString,
1019}
1020
1021/// Funding-account bill row.
1022#[derive(Debug, Clone, Default, Deserialize)]
1023#[serde(rename_all = "camelCase")]
1024#[non_exhaustive]
1025pub struct FundingBill {
1026    /// Bill ID.
1027    #[serde(default, rename = "billId")]
1028    pub bill_id: String,
1029    /// Currency code.
1030    #[serde(default)]
1031    pub ccy: String,
1032    /// Balance change.
1033    #[serde(default)]
1034    pub bal_chg: NumberString,
1035    /// Balance after change.
1036    #[serde(default)]
1037    pub bal: NumberString,
1038    /// Bill type.
1039    #[serde(default, rename = "type")]
1040    pub bill_type: String,
1041    /// Timestamp.
1042    #[serde(default)]
1043    pub ts: NumberString,
1044}
1045
1046/// Lightning deposit invoice.
1047#[derive(Debug, Clone, Default, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049#[non_exhaustive]
1050pub struct DepositLightning {
1051    /// Currency code.
1052    #[serde(default)]
1053    pub ccy: String,
1054    /// Invoice amount.
1055    #[serde(default)]
1056    pub amt: NumberString,
1057    /// Lightning invoice.
1058    #[serde(default)]
1059    pub invoice: String,
1060    /// Recipient.
1061    #[serde(default)]
1062    pub to: String,
1063}
1064
1065/// Lightning withdrawal result.
1066#[derive(Debug, Clone, Default, Deserialize)]
1067#[serde(rename_all = "camelCase")]
1068#[non_exhaustive]
1069pub struct WithdrawalLightning {
1070    /// Currency code.
1071    #[serde(default)]
1072    pub ccy: String,
1073    /// Withdrawal amount.
1074    #[serde(default)]
1075    pub amt: NumberString,
1076    /// Withdrawal ID.
1077    #[serde(default, rename = "wdId")]
1078    pub wd_id: String,
1079}
1080
1081/// Total asset valuation.
1082#[derive(Debug, Clone, Default, Deserialize)]
1083#[serde(rename_all = "camelCase")]
1084#[non_exhaustive]
1085pub struct AssetValuation {
1086    /// Valuation details by account area.
1087    #[serde(default)]
1088    pub details: AssetValuationDetails,
1089    /// Total balance in the requested valuation currency.
1090    #[serde(default, rename = "totalBal")]
1091    pub total_bal: NumberString,
1092}
1093
1094/// Asset valuation details by account area.
1095#[derive(Debug, Clone, Default, Deserialize)]
1096#[serde(rename_all = "camelCase")]
1097#[non_exhaustive]
1098pub struct AssetValuationDetails {
1099    /// Funding-account valuation.
1100    #[serde(default)]
1101    pub funding: NumberString,
1102    /// Trading-account valuation.
1103    #[serde(default)]
1104    pub trading: NumberString,
1105    /// Earn-account valuation.
1106    #[serde(default)]
1107    pub earn: NumberString,
1108    /// Classic-account valuation.
1109    #[serde(default)]
1110    pub classic: NumberString,
1111}
1112
1113/// Deposit/withdrawal status row.
1114#[derive(Debug, Clone, Default, Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116#[non_exhaustive]
1117pub struct DepositWithdrawStatus {
1118    /// Withdrawal ID.
1119    #[serde(default, rename = "wdId")]
1120    pub wd_id: String,
1121    /// Transaction ID.
1122    #[serde(default, rename = "txId")]
1123    pub tx_id: String,
1124    /// Currency code.
1125    #[serde(default)]
1126    pub ccy: String,
1127    /// Chain name.
1128    #[serde(default)]
1129    pub chain: String,
1130    /// Destination address.
1131    #[serde(default)]
1132    pub to: String,
1133    /// State.
1134    #[serde(default)]
1135    pub state: String,
1136}
1137
1138/// Dust-conversion result.
1139#[derive(Debug, Clone, Default, Deserialize)]
1140#[serde(rename_all = "camelCase")]
1141#[non_exhaustive]
1142pub struct ConvertDustAssetsResult {
1143    /// Currency code.
1144    #[serde(default)]
1145    pub ccy: String,
1146    /// Converted amount.
1147    #[serde(default)]
1148    pub amt: NumberString,
1149}
1150
1151/// Purchase/redemption result.
1152#[derive(Debug, Clone, Default, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154#[non_exhaustive]
1155pub struct PurchaseRedemptResult {
1156    /// Currency code.
1157    #[serde(default)]
1158    pub ccy: String,
1159    /// Amount.
1160    #[serde(default)]
1161    pub amt: NumberString,
1162    /// Operation side.
1163    #[serde(default)]
1164    pub side: String,
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170
1171    #[test]
1172    fn funds_transfer_request_omits_unset_optional_fields() {
1173        let value =
1174            serde_json::to_value(FundsTransferRequest::new("USDT", "1", "6", "18")).unwrap();
1175
1176        assert_eq!(value["ccy"], "USDT");
1177        assert_eq!(value["amt"], "1");
1178        assert_eq!(value["from"], "6");
1179        assert_eq!(value["to"], "18");
1180        assert!(value.get("subAcct").is_none());
1181        assert!(value.get("instId").is_none());
1182        assert!(value.get("loanTrans").is_none());
1183    }
1184
1185    #[test]
1186    fn withdrawal_request_omits_unset_optional_fields() {
1187        let value =
1188            serde_json::to_value(WithdrawalRequest::new("USDT", "1", "3", "example")).unwrap();
1189
1190        assert_eq!(value["ccy"], "USDT");
1191        assert_eq!(value["amt"], "1");
1192        assert_eq!(value["dest"], "3");
1193        assert_eq!(value["toAddr"], "example");
1194        assert!(value.get("chain").is_none());
1195        assert!(value.get("areaCode").is_none());
1196        assert!(value.get("toAddrType").is_none());
1197    }
1198
1199    #[test]
1200    fn history_requests_omit_unset_optional_fields() {
1201        let deposit = serde_urlencoded::to_string(DepositHistoryRequest::new().limit(5)).unwrap();
1202        assert_eq!(deposit, "limit=5");
1203
1204        let withdrawal =
1205            serde_urlencoded::to_string(WithdrawalHistoryRequest::new().to_addr_type("1")).unwrap();
1206        assert_eq!(withdrawal, "toAddrType=1");
1207    }
1208
1209    #[test]
1210    fn deposit_withdraw_status_request_omits_unset_optional_fields() {
1211        let query = serde_urlencoded::to_string(
1212            DepositWithdrawStatusRequest::new()
1213                .currency("USDT")
1214                .chain("USDT-TRC20"),
1215        )
1216        .unwrap();
1217
1218        assert_eq!(query, "ccy=USDT&chain=USDT-TRC20");
1219    }
1220}