ccxt_exchanges/binance/parser/
transaction.rs

1#![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
14/// Check if a currency is a fiat currency.
15pub 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
22/// Extract internal transfer ID from transaction ID.
23pub 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
29/// Parse transaction status based on transaction type.
30pub 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
60/// Parse transaction (deposit or withdrawal) from Binance API response.
61pub 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
183/// Parse transfer record data from Binance universal transfer API.
184pub 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
241/// Parse deposit address from Binance API response.
242pub 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
285/// Parse deposit and withdrawal fee information from Binance API.
286pub 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
336/// Parse network information from Binance API.
337pub 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
381/// Parse multiple deposit and withdrawal fee information entries from Binance API.
382pub 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
395/// Parse futures transfer type code from Binance API.
396pub 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}