kucoin 0.7.4

A robust and asynchronous Rust client for the KuCoin exchange API.
Documentation
use crate::{
    client::rest::KuCoinClient,
    types::{
        KuCoinResponse,
        withdraw::{WithdrawRequest, WithdrawResponse, WithdrawType},
    },
    utils::errors::{KucoinErrors, KucoinResults},
};
use log::{debug, error, warn};
use serde_json::to_string;
use std::cmp::min; // Required for capping the delay time
use tokio::time::{Duration, sleep}; // Required for delays // Assuming you use the 'log' crate

pub struct WithdrawHandler<'a> {
    pub client: &'a KuCoinClient,
}

impl WithdrawRequest {
    /// Creates a new withdrawal request.
    pub fn new(currency: &str, to_address: &str, amount: f64, withdraw_type: WithdrawType) -> Self {
        WithdrawRequest {
            amount: amount.to_string(),
            chain: Some("eth".to_string()),
            currency: currency.to_string(),
            fee_deduct_type: None,
            is_inner: Some(false),
            memo: None,
            remark: None,
            to_address: to_address.to_string(),
            withdraw_type,
        }
    }

    pub fn set_chain(mut self, chain: &str) -> Self {
        self.chain = Some(chain.to_string());
        self
    }

    pub fn set_memo(mut self, memo: &str) -> Self {
        self.memo = Some(memo.to_string());
        self
    }

    pub fn set_isinner(mut self) -> Self {
        self.is_inner = Some(true);
        self
    }

    pub fn set_remark(mut self, rm: &str) -> Self {
        self.remark = Some(rm.to_string());
        self
    }

    pub fn set_fee_deduct_type(mut self, type_: &str) -> Self {
        self.fee_deduct_type = Some(type_.to_string());
        self
    }
}

impl<'a> WithdrawHandler<'a> {
    /// Executes the withdrawal request with built-in Exponential Backoff.
    pub async fn execute(
        &self,
        req: WithdrawRequest,
    ) -> KucoinResults<KuCoinResponse<WithdrawResponse>> {
        // 1. Serialize once. We can reuse this payload string for every retry.
        let payload = serde_json::to_string(&req)?;
        let endpoint = "/api/v3/withdrawals";

        // 2. Retry Configuration
        let max_retries = 8;
        let mut attempts = 0;
        let mut current_delay = 2; // Start with 2 seconds

        loop {
            // We use the same payload reference &payload
            let result = self
                .client
                .send::<KuCoinResponse<WithdrawResponse>>("POST", &payload, endpoint)
                .await;

            match result {
                Ok(res) => {
                    // Success! Return immediately.
                    if res.code.contains("110001") {
                        debug!(
                            target: "withdarw",
                            "SYSTEM_BUSY {:?}", payload
                        );
                        continue;
                    }
                    return Ok(res);
                }
                Err(e) => {
                    let err_msg = e.to_string();

                    // 3. Check for specific "System Busy" errors
                    if err_msg.contains("110001") || err_msg.contains("SYSTEM_BUSY") {
                        attempts += 1;

                        if attempts >= max_retries {
                            let msg = format!(
                                "KuCoin API Busy - Max Retries Reached ({}) | error={}",
                                attempts, err_msg
                            );
                            error!(target: "withdraw", "{}", msg);
                            // Return the last error encountered
                            return Err(KucoinErrors::MissingIsolatedTag(e.to_string()));
                        }

                        warn!(
                            target: "withdraw",
                            "System busy (110001). Attempt {}/{}. Retrying in {}s...",
                            attempts, max_retries, current_delay
                        );

                        // 4. Wait (Backoff)
                        sleep(Duration::from_secs(current_delay)).await;

                        // 5. Increase delay for next time (Exponential), capped at 60s
                        current_delay = min(current_delay * 2, 60);

                        continue; // Restart loop
                    }

                    // If it's NOT a system busy error (e.g. "Invalid Address"), fail immediately.
                    return Err(KucoinErrors::ReqwestError(e));
                }
            }
        }
    }
}