ccxt_exchanges/binance/parser/
transaction.rs1#![allow(dead_code)]
2
3use ccxt_core::{
4 Result,
5 error::{Error, ParseError},
6 types::{
7 DepositAddress, Transaction, TransactionFee, TransactionStatus, TransactionType, Transfer,
8 },
9};
10use rust_decimal::Decimal;
11use rust_decimal::prelude::FromStr;
12use serde_json::Value;
13
14pub fn is_fiat_currency(currency: &str) -> bool {
16 matches!(
17 currency.to_uppercase().as_str(),
18 "USD" | "EUR" | "GBP" | "JPY" | "CNY" | "KRW" | "AUD" | "CAD" | "CHF" | "HKD" | "SGD"
19 )
20}
21
22pub fn extract_internal_transfer_id(txid: &str) -> String {
24 const PREFIX: &str = "Internal transfer ";
25 txid.strip_prefix(PREFIX)
26 .map_or_else(|| txid.to_string(), ToString::to_string)
27}
28
29pub fn parse_transaction_status_by_type(
31 status_value: &Value,
32 is_deposit: bool,
33) -> ccxt_core::types::TransactionStatus {
34 if let Some(status_int) = status_value.as_i64() {
35 if is_deposit {
36 match status_int {
37 1 | 6 => TransactionStatus::Ok,
38 _ => TransactionStatus::Pending,
39 }
40 } else {
41 match status_int {
42 1 => TransactionStatus::Canceled,
43 3 | 5 => TransactionStatus::Failed,
44 6 => TransactionStatus::Ok,
45 _ => TransactionStatus::Pending,
46 }
47 }
48 } else if let Some(status_str) = status_value.as_str() {
49 match status_str {
50 "Failed" | "Refund Failed" => TransactionStatus::Failed,
51 "Successful" => TransactionStatus::Ok,
52 "Refunding" | "Refunded" => TransactionStatus::Canceled,
53 _ => TransactionStatus::Pending,
54 }
55 } else {
56 TransactionStatus::Pending
57 }
58}
59
60pub fn parse_transaction(
62 data: &Value,
63 transaction_type: ccxt_core::types::TransactionType,
64) -> Result<ccxt_core::types::Transaction> {
65 let is_deposit = matches!(transaction_type, TransactionType::Deposit);
66
67 let id = if is_deposit {
68 data["id"]
69 .as_str()
70 .or_else(|| data["orderNo"].as_str())
71 .unwrap_or("")
72 .to_string()
73 } else {
74 data["id"]
75 .as_str()
76 .or_else(|| data["withdrawOrderId"].as_str())
77 .unwrap_or("")
78 .to_string()
79 };
80
81 let currency = data["coin"]
82 .as_str()
83 .or_else(|| data["fiatCurrency"].as_str())
84 .unwrap_or("")
85 .to_string();
86
87 let amount = data["amount"]
88 .as_str()
89 .and_then(|s| Decimal::from_str(s).ok())
90 .unwrap_or(Decimal::ZERO);
91
92 let fee = if is_deposit {
93 None
94 } else {
95 data["transactionFee"]
96 .as_str()
97 .or_else(|| data["totalFee"].as_str())
98 .and_then(|s| Decimal::from_str(s).ok())
99 .map(|cost| TransactionFee {
100 currency: currency.clone(),
101 cost,
102 })
103 };
104
105 let timestamp = if is_deposit {
106 data["insertTime"].as_i64()
107 } else {
108 data["createTime"].as_i64().or_else(|| {
109 data["applyTime"].as_str().and_then(|s| {
110 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
111 .ok()
112 .map(|dt| dt.and_utc().timestamp_millis())
113 })
114 })
115 };
116
117 let datetime = timestamp.and_then(|ts| ccxt_core::time::iso8601(ts).ok());
118
119 let network = data["network"].as_str().map(ToString::to_string);
120
121 let address = data["address"]
122 .as_str()
123 .or_else(|| data["depositAddress"].as_str())
124 .map(ToString::to_string);
125
126 let tag = data["addressTag"]
127 .as_str()
128 .or_else(|| data["tag"].as_str())
129 .filter(|s| !s.is_empty())
130 .map(ToString::to_string);
131
132 let mut txid = data["txId"]
133 .as_str()
134 .or_else(|| data["hash"].as_str())
135 .map(ToString::to_string);
136
137 let transfer_type = data["transferType"].as_i64();
138 let is_internal = transfer_type == Some(1);
139
140 if is_internal {
141 if let Some(ref tx) = txid {
142 txid = Some(extract_internal_transfer_id(tx));
143 }
144 }
145
146 let status = if let Some(status_value) = data.get("status") {
147 parse_transaction_status_by_type(status_value, is_deposit)
148 } else {
149 TransactionStatus::Pending
150 };
151
152 let updated = data["updateTime"].as_i64();
153
154 let comment = data["info"]
155 .as_str()
156 .or_else(|| data["comment"].as_str())
157 .map(ToString::to_string);
158
159 Ok(Transaction {
160 info: Some(data.clone()),
161 id,
162 txid,
163 timestamp,
164 datetime,
165 network,
166 address: address.clone(),
167 address_to: if is_deposit { address.clone() } else { None },
168 address_from: if is_deposit { None } else { address },
169 tag: tag.clone(),
170 tag_to: if is_deposit { tag.clone() } else { None },
171 tag_from: if is_deposit { None } else { tag },
172 transaction_type,
173 amount,
174 currency,
175 status,
176 updated,
177 internal: Some(is_internal),
178 comment,
179 fee,
180 })
181}
182
183pub fn parse_transfer(data: &Value) -> Result<Transfer> {
185 let id = data["tranId"]
186 .as_i64()
187 .or_else(|| data["txId"].as_i64())
188 .or_else(|| data["transactionId"].as_i64())
189 .map(|id| id.to_string());
190
191 let timestamp = data["timestamp"]
192 .as_i64()
193 .or_else(|| data["transactionTime"].as_i64())
194 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
195
196 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
197 .map(|dt| dt.to_rfc3339())
198 .unwrap_or_default();
199
200 let currency = data["asset"]
201 .as_str()
202 .or_else(|| data["currency"].as_str())
203 .ok_or_else(|| Error::from(ParseError::missing_field("asset")))?
204 .to_string();
205
206 let amount = if let Some(amount_str) = data["amount"].as_str() {
207 amount_str.parse::<f64>().unwrap_or(0.0)
208 } else {
209 data["amount"].as_f64().unwrap_or(0.0)
210 };
211
212 let mut from_account = data["fromAccountType"].as_str().map(ToString::to_string);
213
214 let mut to_account = data["toAccountType"].as_str().map(ToString::to_string);
215
216 if from_account.is_none() || to_account.is_none() {
217 if let Some(type_str) = data["type"].as_str() {
218 let parts: Vec<&str> = type_str.split('_').collect();
219 if parts.len() == 2 {
220 from_account = Some(parts[0].to_lowercase());
221 to_account = Some(parts[1].to_lowercase());
222 }
223 }
224 }
225
226 let status = data["status"].as_str().unwrap_or("SUCCESS").to_lowercase();
227
228 Ok(Transfer {
229 id,
230 timestamp,
231 datetime,
232 currency,
233 amount,
234 from_account,
235 to_account,
236 status,
237 info: Some(data.clone()),
238 })
239}
240
241pub fn parse_deposit_address(data: &Value) -> Result<ccxt_core::types::DepositAddress> {
243 let currency = data["coin"]
244 .as_str()
245 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
246 .to_string();
247
248 let address = data["address"]
249 .as_str()
250 .ok_or_else(|| Error::from(ParseError::missing_field("address")))?
251 .to_string();
252
253 let network = data["network"]
254 .as_str()
255 .map(ToString::to_string)
256 .or_else(|| {
257 data["url"].as_str().and_then(|url| {
258 if url.contains("btc.com") {
259 Some("BTC".to_string())
260 } else if url.contains("etherscan.io") {
261 Some("ETH".to_string())
262 } else if url.contains("tronscan.org") {
263 Some("TRX".to_string())
264 } else {
265 None
266 }
267 })
268 });
269
270 let tag = data["tag"]
271 .as_str()
272 .or_else(|| data["addressTag"].as_str())
273 .filter(|s| !s.is_empty())
274 .map(ToString::to_string);
275
276 Ok(DepositAddress {
277 info: Some(data.clone()),
278 currency,
279 network,
280 address,
281 tag,
282 })
283}
284
285pub fn parse_deposit_withdraw_fee(data: &Value) -> Result<ccxt_core::types::DepositWithdrawFee> {
287 let currency = data["coin"]
288 .as_str()
289 .ok_or_else(|| Error::from(ParseError::missing_field("coin")))?
290 .to_string();
291
292 let mut networks = Vec::new();
293 let mut withdraw_fee = 0.0;
294 let mut withdraw_min = 0.0;
295 let mut withdraw_max = 0.0;
296 let mut deposit_enable = false;
297 let mut withdraw_enable = false;
298
299 if let Some(network_list) = data["networkList"].as_array() {
300 for network_data in network_list {
301 let network = parse_network_info(network_data)?;
302
303 if network_data["isDefault"].as_bool().unwrap_or(false) {
304 withdraw_fee = network.withdraw_fee;
305 withdraw_min = network.withdraw_min;
306 withdraw_max = network.withdraw_max;
307 deposit_enable = network.deposit_enable;
308 withdraw_enable = network.withdraw_enable;
309 }
310
311 networks.push(network);
312 }
313 }
314
315 if !networks.is_empty() && withdraw_fee == 0.0 {
316 let first = &networks[0];
317 withdraw_fee = first.withdraw_fee;
318 withdraw_min = first.withdraw_min;
319 withdraw_max = first.withdraw_max;
320 deposit_enable = first.deposit_enable;
321 withdraw_enable = first.withdraw_enable;
322 }
323
324 Ok(ccxt_core::types::DepositWithdrawFee {
325 currency,
326 withdraw_fee,
327 withdraw_min,
328 withdraw_max,
329 deposit_enable,
330 withdraw_enable,
331 networks,
332 info: Some(data.clone()),
333 })
334}
335
336pub fn parse_network_info(data: &Value) -> Result<ccxt_core::types::NetworkInfo> {
338 let network = data["network"]
339 .as_str()
340 .ok_or_else(|| Error::from(ParseError::missing_field("network")))?
341 .to_string();
342
343 let name = data["name"].as_str().unwrap_or(&network).to_string();
344
345 let withdraw_fee = data["withdrawFee"]
346 .as_str()
347 .and_then(|s| s.parse::<f64>().ok())
348 .or_else(|| data["withdrawFee"].as_f64())
349 .unwrap_or(0.0);
350
351 let withdraw_min = data["withdrawMin"]
352 .as_str()
353 .and_then(|s| s.parse::<f64>().ok())
354 .or_else(|| data["withdrawMin"].as_f64())
355 .unwrap_or(0.0);
356
357 let withdraw_max = data["withdrawMax"]
358 .as_str()
359 .and_then(|s| s.parse::<f64>().ok())
360 .or_else(|| data["withdrawMax"].as_f64())
361 .unwrap_or(0.0);
362
363 let deposit_enable = data["depositEnable"].as_bool().unwrap_or(false);
364 let withdraw_enable = data["withdrawEnable"].as_bool().unwrap_or(false);
365 let deposit_confirmations = data["minConfirm"].as_u64().map(|v| v as u32);
366 let withdraw_confirmations = data["unlockConfirm"].as_u64().map(|v| v as u32);
367
368 Ok(ccxt_core::types::NetworkInfo {
369 network,
370 name,
371 withdraw_fee,
372 withdraw_min,
373 withdraw_max,
374 deposit_enable,
375 withdraw_enable,
376 deposit_confirmations,
377 withdraw_confirmations,
378 })
379}
380
381pub fn parse_deposit_withdraw_fees(
383 data: &Value,
384) -> Result<Vec<ccxt_core::types::DepositWithdrawFee>> {
385 if let Some(array) = data.as_array() {
386 array
387 .iter()
388 .map(|item| parse_deposit_withdraw_fee(item))
389 .collect()
390 } else {
391 Ok(vec![parse_deposit_withdraw_fee(data)?])
392 }
393}
394
395pub fn parse_futures_transfer_type(transfer_type: i32) -> Result<(&'static str, &'static str)> {
397 match transfer_type {
398 1 => Ok(("spot", "future")),
399 2 => Ok(("future", "spot")),
400 3 => Ok(("spot", "delivery")),
401 4 => Ok(("delivery", "spot")),
402 _ => Err(Error::invalid_request(format!(
403 "Invalid futures transfer type: {}. Must be between 1 and 4",
404 transfer_type
405 ))),
406 }
407}