1use crate::auth::Signer;
2use crate::client::IndodaxClient;
3use crate::commands::helpers;
4use crate::config::IndodaxConfig;
5use crate::output::CommandOutput;
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, clap::Subcommand)]
11pub enum AccountCommand {
12 #[command(name = "info", about = "Get account information and balances")]
13 Info,
14
15 #[command(name = "balance", about = "Show account balances")]
16 Balance,
17
18 #[command(name = "open-orders", about = "List open orders")]
19 OpenOrders {
20 #[arg(short, long, help = "Filter by trading pair")]
21 pair: Option<String>,
22 },
23
24 #[command(name = "order-history", about = "Get order history (v2 API)")]
25 OrderHistory {
26 #[arg(short, long, default_value = "btc_idr")]
27 symbol: String,
28 #[arg(short, long, default_value = "100")]
29 limit: u32,
30 },
31
32 #[command(name = "trade-history", about = "Get trade fill history (v2 API)")]
33 TradeHistory {
34 #[arg(short, long, default_value = "btc_idr")]
35 symbol: String,
36 #[arg(short, long, default_value = "100")]
37 limit: u32,
38 },
39
40 #[command(name = "trans-history", about = "Get deposit and withdrawal history")]
41 TransHistory,
42
43 #[command(name = "get-order", about = "Get order details by order ID")]
44 GetOrder {
45 #[arg(long)]
46 order_id: u64,
47 #[arg(long)]
48 pair: String,
49 },
50
51 #[command(name = "equity-snap", about = "Record a portfolio equity snapshot")]
52 EquitySnap,
53
54 #[command(name = "equity-history", about = "View equity snapshot history")]
55 EquityHistory {
56 #[arg(short, long, default_value = "20", help = "Number of snapshots to show")]
57 limit: usize,
58 #[arg(long, help = "Show all snapshots")]
59 all: bool,
60 },
61}
62
63pub async fn execute(
64 client: &IndodaxClient,
65 cmd: &AccountCommand,
66) -> Result<CommandOutput> {
67 match cmd {
68 AccountCommand::Info => info(client).await,
69 AccountCommand::Balance => balance(client).await,
70 AccountCommand::OpenOrders { pair } => {
71 let pair = pair.as_ref().map(|p| helpers::normalize_pair(p));
72 open_orders(client, pair.as_deref()).await
73 }
74 AccountCommand::OrderHistory { symbol, limit } => {
75 let symbol = helpers::normalize_pair(symbol);
76 order_history(client, &symbol, *limit).await
77 }
78 AccountCommand::TradeHistory { symbol, limit } => {
79 let symbol = helpers::normalize_pair(symbol);
80 trade_history(client, &symbol, *limit).await
81 }
82 AccountCommand::TransHistory => trans_history(client).await,
83 AccountCommand::GetOrder { order_id, pair } => {
84 let pair = helpers::normalize_pair(pair);
85 get_order(client, *order_id, &pair).await
86 }
87 AccountCommand::EquitySnap => equity_snap(client).await,
88 AccountCommand::EquityHistory { limit, all } => {
89 equity_history(*limit, *all)
90 }
91 }
92}
93
94async fn info(client: &IndodaxClient) -> Result<CommandOutput> {
95 let data: serde_json::Value =
96 client.private_post_v1("getInfo", &HashMap::new()).await?;
97
98 let headers = vec![
99 "Field".into(), "Value".into(),
100 ];
101 let mut rows: Vec<Vec<String>> = vec![
102 vec!["Name".into(), helpers::value_to_string(data.get("name").unwrap_or(&serde_json::Value::Null))],
103 vec!["User ID".into(), helpers::value_to_string(data.get("user_id").unwrap_or(&serde_json::Value::Null))],
104 vec!["Server Time".into(), helpers::format_timestamp(data["server_time"].as_u64().unwrap_or(0), true)],
105 vec!["Vip Level".into(), helpers::value_to_string(data.get("vip_level").unwrap_or(&serde_json::Value::Null))],
106 vec!["Verified".into(), helpers::value_to_string(data.get("verified_user").unwrap_or(&serde_json::Value::Null))],
107 ];
108
109 let balance = &data["balance"];
110 if let serde_json::Value::Object(bal_map) = balance {
111 let mut entries: Vec<(&String, &serde_json::Value)> = bal_map.iter().collect();
112 entries.sort_by(|a, b| a.0.cmp(b.0));
113 for (k, v) in entries {
114 rows.push(vec![k.clone(), helpers::value_to_string(v)]);
115 }
116 }
117
118 Ok(CommandOutput::new(data, headers, rows))
119}
120
121async fn balance(client: &IndodaxClient) -> Result<CommandOutput> {
122 let data: serde_json::Value =
123 client.private_post_v1("getInfo", &HashMap::new()).await?;
124
125 let balance = &data["balance"];
126 let headers = vec!["Currency".into(), "Balance".into()];
127 let mut rows: Vec<Vec<String>> = Vec::new();
128
129 if let serde_json::Value::Object(bal_map) = balance {
130 let mut entries: Vec<(String, f64)> = bal_map
131 .iter()
132 .map(|(k, v)| {
133 let val = v.as_str().and_then(|s| s.parse::<f64>().ok())
134 .or_else(|| v.as_f64())
135 .unwrap_or(0.0);
136 (k.clone(), val)
137 })
138 .collect();
139 entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
140 for (currency, amount) in entries {
141 rows.push(vec![currency, amount.to_string()]);
142 }
143 }
144
145 Ok(CommandOutput::new(data, headers, rows))
146}
147
148async fn open_orders(
149 client: &IndodaxClient,
150 pair: Option<&str>,
151) -> Result<CommandOutput> {
152 let mut params = HashMap::new();
153 if let Some(p) = pair {
154 params.insert("pair".into(), p.to_string());
155 }
156 let data: serde_json::Value =
157 client.private_post_v1("openOrders", ¶ms).await?;
158
159 let orders = &data["orders"];
160 let headers = vec![
161 "Order ID".into(), "Pair".into(), "Type".into(), "Side".into(),
162 "Price".into(), "Amount".into(), "Remaining".into(), "Time".into(),
163 ];
164 let mut rows: Vec<Vec<String>> = Vec::new();
165
166 if let serde_json::Value::Object(orders_map) = orders {
167 for (order_id, order_val) in orders_map {
168 let pair = helpers::value_to_string(
169 priv_get(order_val, &["pair", "market", "symbol"]),
170 );
171 let order_type = helpers::value_to_string(
172 priv_get(order_val, &["type", "order_type"]),
173 );
174 let side = if order_type.to_lowercase().contains("sell") {
175 "SELL"
176 } else {
177 "BUY"
178 };
179
180 let remaining = helpers::value_to_string(
181 priv_get(order_val, &["remaining", "remain_volume", "remaining_volume"]),
182 );
183 let base_amount = order_val.get("order_btc")
184 .or_else(|| order_val.get("order_base"))
185 .or_else(|| order_val.get("amount"))
186 .map(helpers::value_to_string)
187 .unwrap_or_default();
188
189 let time_val = order_val.get("submit_time")
190 .or_else(|| order_val.get("created_at"))
191 .or_else(|| order_val.get("time"))
192 .map(|v| {
193 let ts = v.as_u64().unwrap_or(0);
194 if ts > 1_000_000_000_000 {
195 helpers::format_timestamp(ts, true)
196 } else {
197 helpers::format_timestamp(ts, false)
198 }
199 })
200 .unwrap_or_default();
201
202 rows.push(vec![
203 order_id.to_string(),
204 pair,
205 order_type,
206 side.into(),
207 helpers::value_to_string(
208 priv_get(order_val, &["price", "order_price"]),
209 ),
210 base_amount,
211 remaining,
212 time_val,
213 ]);
214 }
215 }
216
217 rows.sort_by(|a, b| {
218 match (b[0].parse::<u64>().ok(), a[0].parse::<u64>().ok()) {
219 (Some(bv), Some(av)) => bv.cmp(&av),
220 _ => b[0].cmp(&a[0]),
221 }
222 });
223 let count = rows.len();
224 Ok(CommandOutput::new(data, headers, rows)
225 .with_addendum(format!("{} open orders", count)))
226}
227
228async fn order_history(
229 client: &IndodaxClient,
230 symbol: &str,
231 limit: u32,
232) -> Result<CommandOutput> {
233 let now = Signer::now_millis();
234 let start = now - crate::commands::helpers::ONE_DAY_MS;
235
236 let mut params = HashMap::new();
237 params.insert("symbol".into(), crate::commands::helpers::normalize_pair_v2(symbol));
238 params.insert("limit".into(), limit.max(10).to_string());
239 params.insert("startTime".into(), start.to_string());
240 params.insert("endTime".into(), now.to_string());
241
242 let data: serde_json::Value =
243 client.private_get_v2("/api/v2/order/histories", ¶ms).await?;
244
245 let headers = vec![
246 "Order ID".into(), "Symbol".into(), "Side".into(), "Type".into(),
247 "Price".into(), "Qty".into(), "Status".into(), "Time".into(),
248 ];
249 let mut rows: Vec<Vec<String>> = Vec::new();
250
251 if let serde_json::Value::Array(arr) = &data {
252 for order in arr.iter().take(limit as usize) {
253 rows.push(vec![
254 helpers::value_to_string(priv_get(order, &["orderId", "order_id"])),
255 helpers::value_to_string(priv_get(order, &["symbol", "pair"])),
256 helpers::value_to_string(priv_get(order, &["side", "order_side"])),
257 helpers::value_to_string(priv_get(order, &["type", "order_type"])),
258 helpers::value_to_string(priv_get(order, &["price", "order_price"])),
259 helpers::value_to_string(priv_get(order, &["origQty", "orig_qty", "qty"])),
260 helpers::value_to_string(priv_get(order, &["status", "order_status"])),
261 helpers::value_to_string(priv_get(order, &["time", "created_at"])),
262 ]);
263 }
264 }
265
266 Ok(CommandOutput::new(data, headers, rows))
267}
268
269async fn trade_history(
270 client: &IndodaxClient,
271 symbol: &str,
272 limit: u32,
273) -> Result<CommandOutput> {
274 let now = Signer::now_millis();
275 let start = now - crate::commands::helpers::ONE_DAY_MS;
276
277 let mut params = HashMap::new();
278 params.insert("symbol".into(), crate::commands::helpers::normalize_pair_v2(symbol));
279 params.insert("limit".into(), limit.max(10).to_string());
280 params.insert("startTime".into(), start.to_string());
281 params.insert("endTime".into(), now.to_string());
282
283 let data: serde_json::Value =
284 client.private_get_v2("/api/v2/myTrades", ¶ms).await?;
285
286 let headers = vec![
287 "Trade ID".into(), "Order ID".into(), "Symbol".into(), "Side".into(),
288 "Price".into(), "Qty".into(), "Fee".into(), "Time".into(),
289 ];
290 let mut rows: Vec<Vec<String>> = Vec::new();
291
292 if let serde_json::Value::Array(arr) = &data {
293 for trade in arr.iter().take(limit as usize) {
294 rows.push(vec![
295 helpers::value_to_string(priv_get(trade, &["id", "tradeId", "trade_id"])),
296 helpers::value_to_string(priv_get(trade, &["orderId", "order_id"])),
297 helpers::value_to_string(priv_get(trade, &["symbol", "pair"])),
298 helpers::value_to_string(priv_get(trade, &["side"])),
299 helpers::value_to_string(priv_get(trade, &["price"])),
300 helpers::value_to_string(priv_get(trade, &["qty", "quantity"])),
301 helpers::value_to_string(priv_get(trade, &["commission", "fee"])),
302 helpers::value_to_string(priv_get(trade, &["time", "timestamp"])),
303 ]);
304 }
305 }
306
307 Ok(CommandOutput::new(data, headers, rows))
308}
309
310async fn trans_history(client: &IndodaxClient) -> Result<CommandOutput> {
311 let data: serde_json::Value =
312 client.private_post_v1("transHistory", &HashMap::new()).await?;
313
314 let headers = vec![
315 "ID".into(), "Type".into(), "Currency".into(), "Amount".into(),
316 "Fee".into(), "Status".into(), "Time".into(),
317 ];
318 let mut rows: Vec<Vec<String>> = Vec::new();
319
320 let mut all_trans = Vec::new();
321
322 if let Some(obj) = data.get("withdraw").and_then(|v| v.as_object()) {
323 for (id, val) in obj {
324 all_trans.push((id, "WITHDRAW", val));
325 }
326 }
327 if let Some(obj) = data.get("deposit").and_then(|v| v.as_object()) {
328 for (id, val) in obj {
329 all_trans.push((id, "DEPOSIT", val));
330 }
331 }
332 if let Some(obj) = data.get("transactions").and_then(|v| v.as_object()) {
333 for (id, val) in obj {
334 let tx_type = if id.contains("withdraw") {
335 "WITHDRAW"
336 } else if id.contains("deposit") {
337 "DEPOSIT"
338 } else {
339 "TRANS"
340 };
341 all_trans.push((id, tx_type, val));
342 }
343 }
344
345 for (id, tx_type, entry) in all_trans {
346 rows.push(vec![
347 id.clone(),
348 tx_type.into(),
349 helpers::value_to_string(
350 priv_get(entry, &["currency", "asset", "coin"]),
351 ),
352 helpers::value_to_string(
353 priv_get(entry, &["amount", "value"]),
354 ),
355 helpers::value_to_string(
356 priv_get(entry, &["fee", "withdraw_fee"]),
357 ),
358 helpers::value_to_string(
359 priv_get(entry, &["status", "state"]),
360 ),
361 helpers::value_to_string(
362 priv_get(entry, &["submit_time", "timestamp", "time", "submitted"]),
363 ),
364 ]);
365 }
366
367 rows.sort_by(|a, b| b[0].cmp(&a[0]));
368 Ok(CommandOutput::new(data, headers, rows))
369}
370
371async fn get_order(
372 client: &IndodaxClient,
373 order_id: u64,
374 pair: &str,
375) -> Result<CommandOutput> {
376 let mut params = HashMap::new();
377 params.insert("order_id".into(), order_id.to_string());
378 params.insert("pair".into(), pair.to_string());
379
380 let data: serde_json::Value =
381 client.private_post_v1("getOrder", ¶ms).await?;
382
383 let (headers, rows) = helpers::flatten_json_to_table(&data);
384 Ok(CommandOutput::new(data, headers, rows))
385}
386
387fn priv_get<'a>(val: &'a serde_json::Value, keys: &[&str]) -> &'a serde_json::Value {
388 helpers::first_of(val, keys)
389}
390
391#[derive(Debug, Serialize, Deserialize)]
396struct EquitySnapshot {
397 timestamp: u64,
398 equity: f64,
399}
400
401#[derive(Debug, Serialize, Deserialize)]
402struct EquityHistoryData {
403 snapshots: Vec<EquitySnapshot>,
404}
405
406fn equity_history_path() -> std::path::PathBuf {
407 IndodaxConfig::config_dir().join("equity_history.json")
408}
409
410fn load_equity_history() -> EquityHistoryData {
411 let path = equity_history_path();
412 if path.exists() {
413 std::fs::read_to_string(&path)
414 .ok()
415 .and_then(|s| serde_json::from_str(&s).ok())
416 .unwrap_or(EquityHistoryData { snapshots: vec![] })
417 } else {
418 EquityHistoryData { snapshots: vec![] }
419 }
420}
421
422fn save_equity_history(data: &EquityHistoryData) -> Result<()> {
423 let dir = IndodaxConfig::config_dir();
424 std::fs::create_dir_all(&dir)?;
425 let content = serde_json::to_string_pretty(data)?;
426 #[cfg(unix)]
427 {
428 use std::io::Write;
429 use std::os::unix::fs::OpenOptionsExt;
430 let mut file = std::fs::OpenOptions::new()
431 .write(true)
432 .create(true)
433 .truncate(true)
434 .mode(0o600)
435 .open(equity_history_path())?;
436 file.write_all(content.as_bytes())?;
437 }
438 #[cfg(not(unix))]
439 {
440 std::fs::write(equity_history_path(), content)?;
441 }
442 Ok(())
443}
444
445async fn calculate_equity(client: &IndodaxClient) -> Result<f64> {
446 let info: serde_json::Value = client.private_post_v1("getInfo", &HashMap::new()).await?;
447
448 let mut balances: HashMap<String, f64> = HashMap::new();
449 if let Some(bal_map) = info["balance"].as_object() {
450 for (k, v) in bal_map {
451 let val = v
452 .as_str()
453 .and_then(|s| s.parse::<f64>().ok())
454 .or_else(|| v.as_f64())
455 .unwrap_or(0.0);
456 if val > 0.0 {
457 balances.insert(k.clone(), val);
458 }
459 }
460 }
461
462 let tickers: serde_json::Value = client.public_get("/api/ticker_all").await?;
463 let mut prices: HashMap<String, f64> = HashMap::new();
464 if let Some(t) = tickers["tickers"].as_object() {
465 for (k, v) in t {
466 let last = v["last"]
467 .as_str()
468 .and_then(|s| s.parse::<f64>().ok())
469 .or_else(|| v["last"].as_f64())
470 .unwrap_or(0.0);
471 prices.insert(k.clone(), last);
472 }
473 }
474
475 let mut total = 0.0;
476 let btc_idr = prices.get("btc_idr").copied().unwrap_or(0.0);
477 let usdt_idr = prices.get("usdt_idr").copied().unwrap_or(0.0);
478 let eth_idr = prices.get("eth_idr").copied().unwrap_or(0.0);
479
480 for (currency, amount) in &balances {
481 if currency == "idr" {
482 total += amount;
483 } else if currency == "btc" {
484 total += amount * btc_idr;
485 } else if currency == "usdt" {
486 total += amount * usdt_idr;
487 } else {
488 let pair_idr = format!("{}_{}", currency, "idr");
489 let pair_btc = format!("{}_{}", currency, "btc");
490 let pair_usdt = format!("{}_{}", currency, "usdt");
491 let pair_eth = format!("{}_{}", currency, "eth");
492
493 if let Some(price) = prices.get(&pair_idr) {
494 total += amount * price;
495 } else if let Some(price) = prices.get(&pair_btc) {
496 total += amount * price * btc_idr;
497 } else if let Some(price) = prices.get(&pair_usdt) {
498 total += amount * price * usdt_idr;
499 } else if let Some(price) = prices.get(&pair_eth) {
500 total += amount * price * eth_idr;
501 } else {
502 eprintln!("[EQUITY] Warning: No known price pair for {} (value: {}). Contribution set to 0.", currency.to_uppercase(), amount);
503 }
504 }
505 }
506
507 Ok(total)
508}
509
510async fn equity_snap(client: &IndodaxClient) -> Result<CommandOutput> {
511 let equity = calculate_equity(client).await?;
512 let timestamp = Signer::now_millis();
513
514 let snap = EquitySnapshot { timestamp, equity };
515 let mut history = load_equity_history();
516 history.snapshots.push(snap);
517
518 if history.snapshots.len() > 1000 {
519 let keep = history.snapshots.split_off(history.snapshots.len() - 1000);
520 history.snapshots = keep;
521 }
522
523 save_equity_history(&history)?;
524
525 let count = history.snapshots.len();
526 let first_equity = history.snapshots.first().map(|s| s.equity).unwrap_or(equity);
527 let peak = history.snapshots.iter().map(|s| s.equity).fold(0.0_f64, f64::max);
528 let change = equity - first_equity;
529 let change_pct = if first_equity > 0.0 { (change / first_equity) * 100.0 } else { 0.0 };
530 let dd_pct = if peak > 0.0 { ((equity / peak) - 1.0) * 100.0 } else { 0.0 };
531
532 let headers = vec!["Metric".into(), "Value".into()];
533 let formatted_time = helpers::format_timestamp(timestamp, true);
534 let rows = vec![
535 vec!["Time".into(), formatted_time],
536 vec!["Equity (IDR)".into(), format_equity(equity)],
537 vec!["Change".into(), format_change(change)],
538 vec!["Change %".into(), format_change_pct(change_pct)],
539 vec!["Peak (IDR)".into(), format_equity(peak)],
540 vec!["Drawdown %".into(), format_change_pct(dd_pct)],
541 vec!["Total Snapshots".into(), count.to_string()],
542 ];
543
544 let data = serde_json::json!({
545 "timestamp": timestamp,
546 "equity": equity,
547 "change": change,
548 "change_pct": change_pct,
549 "peak": peak,
550 "drawdown_pct": dd_pct,
551 "total_snapshots": count,
552 });
553
554 Ok(CommandOutput::new(data, headers, rows))
555}
556
557fn equity_history(limit: usize, all: bool) -> Result<CommandOutput> {
558 let history = load_equity_history();
559
560 if history.snapshots.is_empty() {
561 return Ok(CommandOutput::json(serde_json::json!({
562 "status": "ok",
563 "message": "No equity snapshots. Use `indodax account equity-snap` to record one.",
564 "snapshots": [],
565 })));
566 }
567
568 let first_equity = history.snapshots.first().map(|s| s.equity).unwrap_or(0.0);
569
570 let headers = vec![
571 "Time".into(),
572 "Equity (IDR)".into(),
573 "Change".into(),
574 "Change %".into(),
575 "Peak (IDR)".into(),
576 "DD %".into(),
577 ];
578
579 let snapshots_to_show: Vec<&EquitySnapshot> = if all {
580 history.snapshots.iter().collect()
581 } else {
582 let take = limit.min(history.snapshots.len());
583 history.snapshots[history.snapshots.len() - take..]
584 .iter()
585 .collect()
586 };
587
588 let mut rows: Vec<Vec<String>> = Vec::new();
589 let mut peak = 0.0_f64;
590
591 for snap in &snapshots_to_show {
592 if snap.equity > peak {
593 peak = snap.equity;
594 }
595 let change = snap.equity - first_equity;
596 let change_pct = if first_equity > 0.0 {
597 (change / first_equity) * 100.0
598 } else {
599 0.0
600 };
601 let dd_pct = if peak > 0.0 {
602 ((snap.equity / peak) - 1.0) * 100.0
603 } else {
604 0.0
605 };
606
607 rows.push(vec![
608 format_timestamp_short(snap.timestamp),
609 format_equity(snap.equity),
610 format_change(change),
611 format_change_pct(change_pct),
612 format_equity(peak),
613 format_change_pct(dd_pct),
614 ]);
615 }
616
617 let data = serde_json::json!({
618 "count": history.snapshots.len(),
619 "first_equity": first_equity,
620 "snapshots": history.snapshots.iter().map(|s| serde_json::json!({
621 "timestamp": s.timestamp,
622 "equity": s.equity,
623 })).collect::<Vec<_>>(),
624 });
625
626 let count = history.snapshots.len();
627 Ok(CommandOutput::new(data, headers, rows)
628 .with_addendum(format!("[EQUITY] {} snapshot(s) total", count)))
629}
630
631fn format_equity(val: f64) -> String {
632 format!("{:>14.2}", val)
633}
634
635fn format_change(val: f64) -> String {
636 if val >= 0.0 {
637 format!("+{:>10.2}", val)
638 } else {
639 format!("{:>11.2}", val)
640 }
641}
642
643fn format_change_pct(val: f64) -> String {
644 if val >= 0.0 {
645 format!("+{:>7.2}%", val)
646 } else {
647 format!("{:>8.2}%", val)
648 }
649}
650
651fn format_timestamp_short(ts: u64) -> String {
652 let ts_sec = ts / 1000;
653 chrono::DateTime::from_timestamp(ts_sec.min(i64::MAX as u64) as i64, 0)
654 .map(|dt| dt.format("%b %d %H:%M:%S").to_string())
655 .unwrap_or_else(|| ts.to_string())
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use serde_json::json;
662 #[test]
663 fn test_priv_get_existing_key() {
664 let val = json!({"name": "Alice", "age": 30});
665 let result = priv_get(&val, &["name"]);
666 assert_eq!(result, &json!("Alice"));
667 }
668
669 #[test]
670 fn test_priv_get_first_key_exists() {
671 let val = json!({"a": 1, "b": 2});
672 let result = priv_get(&val, &["a", "b"]);
673 assert_eq!(result, &json!(1));
674 }
675
676 #[test]
677 fn test_priv_get_second_key_exists() {
678 let val = json!({"a": null, "b": "2"});
679 let result = priv_get(&val, &["a", "b"]);
680 assert_eq!(result, &json!("2"));
682 }
683
684 #[test]
685 fn test_priv_get_no_keys_exist() {
686 let val = json!({"a": 1});
687 let result = priv_get(&val, &["x", "y", "z"]);
688 assert_eq!(result, &serde_json::Value::Null);
689 }
690
691 #[test]
692 fn test_priv_get_with_json_null() {
693 let val = json!(null);
694 let result = priv_get(&val, &["key"]);
695 assert_eq!(result, &serde_json::Value::Null);
696 }
697
698 #[test]
699 fn test_priv_get_empty_keys() {
700 let val = json!({"a": 1});
701 let result = priv_get(&val, &[]);
702 assert_eq!(result, &serde_json::Value::Null);
703 }
704
705 #[test]
706 fn test_priv_get_nested_value() {
707 let val = json!({"data": {"name": "Bob"}});
708 let result = priv_get(&val, &["data"]);
709 assert_eq!(result, &json!({"name": "Bob"}));
710 }
711
712 #[test]
713 fn test_account_command_variants() {
714 let _cmd1 = AccountCommand::Info;
715 let _cmd2 = AccountCommand::Balance;
716 let _cmd3 = AccountCommand::OpenOrders { pair: Some("btc_idr".into()) };
717 let _cmd4 = AccountCommand::OrderHistory { symbol: "btc_idr".into(), limit: 100 };
718 let _cmd5 = AccountCommand::TradeHistory { symbol: "btc_idr".into(), limit: 100 };
719 let _cmd6 = AccountCommand::TransHistory;
720 let _cmd7 = AccountCommand::GetOrder { order_id: 123, pair: "btc_idr".into() };
721 let _cmd8 = AccountCommand::EquitySnap;
722 let _cmd9 = AccountCommand::EquityHistory { limit: 10, all: false };
723 }
724
725 #[test]
726 fn test_priv_get_with_null_first() {
727 let val = json!({"first": null, "second": "value"});
728 let result = priv_get(&val, &["first", "second"]);
729 assert_eq!(result, &json!("value"));
731 }
732
733 #[test]
734 fn test_priv_get_array_value() {
735 let val = json!({"arr": [1, 2, 3]});
736 let result = priv_get(&val, &["arr"]);
737 assert_eq!(result, &json!([1, 2, 3]));
738 }
739
740 #[test]
741 fn test_priv_get_number_value() {
742 let val = json!({"num": 42.5});
743 let result = priv_get(&val, &["num"]);
744 assert_eq!(result, &json!(42.5));
745 }
746
747 #[test]
748 fn test_priv_get_bool_value() {
749 let val = json!({"flag": true});
750 let result = priv_get(&val, &["flag"]);
751 assert_eq!(result, &json!(true));
752 }
753}