1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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));
}
}
}
}
}