#[cfg(feature = "cli")]
use output::{CommandOutput, OutputFormat};
#[cfg(feature = "cli")]
pub mod alerts;
pub mod auth;
pub mod client;
#[cfg(feature = "cli")]
pub mod commands;
pub mod config;
pub mod errors;
pub mod integration;
#[cfg(feature = "mcp")]
pub mod mcp;
#[cfg(feature = "cli")]
pub mod output;
#[cfg(feature = "cli")]
use client::IndodaxClient;
#[cfg(feature = "cli")]
use errors::IndodaxError;
pub use integration::prelude;
pub(crate) fn now_millis() -> u64 {
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() as u64
}
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
#[cfg(feature = "cli")]
use clap::{Parser, Subcommand};
#[cfg(feature = "cli")]
#[derive(Debug, Parser)]
#[command(
name = "indodax",
version,
about = "Command-line interface for the Indodax cryptocurrency exchange",
long_about = None
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[arg(
short = 'o',
long = "output",
default_value = "table",
help = "Output format: table or json",
global = true
)]
pub output: OutputFormat,
#[arg(
long = "api-key",
help = "API key (overrides config file and env var)",
global = true
)]
pub api_key: Option<String>,
#[arg(
long = "api-secret",
help = "API secret (overrides config file and env var)",
global = true
)]
pub api_secret: Option<String>,
#[arg(
long = "api-secret-stdin",
help = "Read API secret from stdin (more secure than --api-secret)",
global = true
)]
pub api_secret_stdin: bool,
#[arg(
short = 'v',
long = "verbose",
help = "Enable verbose output",
global = true
)]
pub verbose: bool,
#[arg(
long = "yes",
alias = "force",
help = "Skip confirmation prompts for destructive operations",
global = true
)]
pub yes: bool,
}
#[cfg(feature = "cli")]
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(hide = true)]
#[command(subcommand)]
Market(commands::market::MarketCommand),
#[command(hide = true)]
#[command(subcommand)]
Account(commands::account::AccountCommand),
#[command(hide = true)]
#[command(subcommand)]
Trade(commands::trade::TradeCommand),
#[command(hide = true)]
#[command(subcommand)]
Funding(commands::funding::FundingCommand),
ServerTime,
Pairs,
Ticker {
#[arg(default_value = "btc_idr")]
pair: String,
},
History {
#[arg(default_value = "btc_idr")]
pair: String,
#[arg(short, long, default_value = "60")]
timeframe: String,
#[arg(short, long, help = "Start timestamp in seconds (default: 24h ago)")]
from: Option<u64>,
#[arg(long, help = "End timestamp in seconds (default: now)")]
to: Option<u64>,
},
TickerAll,
Summaries,
Orderbook {
#[arg(default_value = "btc_idr")]
pair: String,
#[arg(long, default_value = "20", help = "Number of bid/ask levels to show")]
count: usize,
},
Trades {
#[arg(default_value = "btc_idr")]
pair: String,
},
Ohlc {
#[arg(short, long, default_value = "btc_idr")]
pair: String,
#[arg(long, default_value = "60")]
interval: String,
#[arg(short, long, help = "Start timestamp in seconds (default: 24h ago)")]
since: Option<u64>,
#[arg(long, help = "End timestamp in seconds (default: now)")]
to: Option<u64>,
},
Webdata {
#[arg(default_value = "btc_idr")]
pair: String,
},
ChatHistory,
PairsV2 {
#[arg(short, long)]
pair: Option<String>,
},
SearchV2,
TerminalTrade {
#[arg(default_value = "btc_idr")]
pair: String,
},
TerminalMarket {
#[arg(default_value = "btc_idr")]
pair: String,
},
TerminalCategories,
OnrampConfig {
#[arg(default_value = "usdt_idr")]
pair: String,
},
News {
#[arg(default_value = "btc")]
asset: String,
#[arg(short, long, default_value = "1")]
page: u32,
},
PriceIncrements,
AccountInfo,
Balance,
Transactions,
TradesHistory {
pair: String,
#[arg(short, long, default_value = "500")]
limit: usize,
#[arg(long)]
from_id: Option<u64>,
},
#[command(subcommand)]
Order(commands::trade::TradeCommand),
Withdraw {
#[arg(short, long)]
asset: String,
#[arg(short = 'v', long, help = "Amount to withdraw")]
volume: f64,
#[arg(
long,
help = "Crypto destination address (or Indodax username if --username is set)"
)]
address: String,
#[arg(long, help = "Withdraw to Indodax username instead of blockchain")]
username: bool,
#[arg(long, help = "Memo/tag (for currencies that require it)")]
memo: Option<String>,
#[arg(long, help = "Blockchain network")]
network: Option<String>,
#[arg(long, help = "Callback URL for withdrawal confirmation")]
callback_url: Option<String>,
},
#[command(subcommand)]
Withdrawal(WithdrawalSubcommand),
#[command(subcommand)]
Ws(commands::websocket::WebSocketCommand),
#[command(subcommand)]
Paper(commands::paper::PaperCommand),
#[command(subcommand)]
Auth(commands::auth::AuthCommand),
#[command(subcommand)]
Alert(commands::alert::AlertCommand),
Setup,
Shell,
#[cfg(feature = "mcp")]
Mcp {
#[arg(
short = 's',
long = "groups",
default_value = "market,account,paper,auth",
help = "Comma-separated service groups: market, account, trade, funding, paper, auth"
)]
groups: String,
#[arg(
long,
help = "Allow dangerous operations (trade, funding) without acknowledged flag"
)]
allow_dangerous: bool,
},
}
#[cfg(feature = "cli")]
#[derive(Debug, Subcommand)]
pub enum WithdrawalSubcommand {
Fee {
#[arg(short, long)]
asset: String,
#[arg(short, long, help = "Blockchain network (optional)")]
network: Option<String>,
},
#[cfg(feature = "server")]
ServeCallback {
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(
short,
long,
help = "When true, auto-confirms all callback requests. When false, prompts for each request.",
default_value = "false"
)]
auto_ok: bool,
#[arg(
long,
help = "Listen address (default: 127.0.0.1). Use 0.0.0.0 for network access"
)]
listen: Option<String>,
},
}
#[cfg(feature = "cli")]
pub async fn dispatch(
cli: Cli,
client: &IndodaxClient,
config: &mut config::IndodaxConfig,
) -> Result<CommandOutput, IndodaxError> {
let output = match cli.command {
Command::Market(ref cmd) => commands::market::execute(client, cmd)
.await
.map_err(map_anyhow_error)?,
Command::Account(ref cmd) => commands::account::execute(client, cmd)
.await
.map_err(map_anyhow_error)?,
Command::Trade(ref cmd) => commands::trade::execute(client, cmd, cli.yes)
.await
.map_err(map_anyhow_error)?,
Command::Funding(ref cmd) => commands::funding::execute(client, config, cmd, cli.output)
.await
.map_err(map_anyhow_error)?,
Command::ServerTime => {
commands::market::execute(client, &commands::market::MarketCommand::ServerTime)
.await
.map_err(map_anyhow_error)?
}
Command::Pairs => {
commands::market::execute(client, &commands::market::MarketCommand::Pairs)
.await
.map_err(map_anyhow_error)?
}
Command::Ticker { pair } => {
commands::market::execute(client, &commands::market::MarketCommand::Ticker { pair })
.await
.map_err(map_anyhow_error)?
}
Command::History {
pair,
timeframe,
from,
to,
} => commands::market::execute(
client,
&commands::market::MarketCommand::Ohlc {
symbol: pair,
timeframe,
from,
to,
},
)
.await
.map_err(map_anyhow_error)?,
Command::TickerAll => {
commands::market::execute(client, &commands::market::MarketCommand::TickerAll)
.await
.map_err(map_anyhow_error)?
}
Command::Summaries => {
commands::market::execute(client, &commands::market::MarketCommand::Summaries)
.await
.map_err(map_anyhow_error)?
}
Command::Orderbook { pair, count } => commands::market::execute(
client,
&commands::market::MarketCommand::Orderbook {
pair,
levels: count,
},
)
.await
.map_err(map_anyhow_error)?,
Command::Trades { pair } => {
commands::market::execute(client, &commands::market::MarketCommand::Trades { pair })
.await
.map_err(map_anyhow_error)?
}
Command::Ohlc {
pair,
interval,
since,
to,
} => commands::market::execute(
client,
&commands::market::MarketCommand::Ohlc {
symbol: pair,
timeframe: interval,
from: since,
to,
},
)
.await
.map_err(map_anyhow_error)?,
Command::Webdata { pair } => {
commands::market::execute(client, &commands::market::MarketCommand::WebData { pair })
.await
.map_err(map_anyhow_error)?
}
Command::ChatHistory => {
commands::market::execute(client, &commands::market::MarketCommand::ChatHistory)
.await
.map_err(map_anyhow_error)?
}
Command::PairsV2 { pair } => {
commands::market::execute(client, &commands::market::MarketCommand::PairsV2 { pair })
.await
.map_err(map_anyhow_error)?
}
Command::SearchV2 => {
commands::market::execute(client, &commands::market::MarketCommand::SearchV2)
.await
.map_err(map_anyhow_error)?
}
Command::TerminalTrade { pair } => commands::market::execute(
client,
&commands::market::MarketCommand::TerminalTrade { pair },
)
.await
.map_err(map_anyhow_error)?,
Command::TerminalMarket { pair } => commands::market::execute(
client,
&commands::market::MarketCommand::TerminalMarket { pair },
)
.await
.map_err(map_anyhow_error)?,
Command::TerminalCategories => {
commands::market::execute(client, &commands::market::MarketCommand::TerminalCategories)
.await
.map_err(map_anyhow_error)?
}
Command::OnrampConfig { pair } => commands::market::execute(
client,
&commands::market::MarketCommand::OnrampConfig { pair },
)
.await
.map_err(map_anyhow_error)?,
Command::News { asset, page } => commands::market::execute(
client,
&commands::market::MarketCommand::News { asset, page },
)
.await
.map_err(map_anyhow_error)?,
Command::PriceIncrements => {
commands::market::execute(client, &commands::market::MarketCommand::PriceIncrements)
.await
.map_err(map_anyhow_error)?
}
Command::AccountInfo => {
commands::account::execute(client, &commands::account::AccountCommand::Info)
.await
.map_err(map_anyhow_error)?
}
Command::Balance => {
commands::account::execute(client, &commands::account::AccountCommand::Balance)
.await
.map_err(map_anyhow_error)?
}
Command::Transactions => {
commands::account::execute(client, &commands::account::AccountCommand::TransHistory)
.await
.map_err(map_anyhow_error)?
}
Command::TradesHistory {
pair,
limit,
from_id: _,
} => commands::account::execute(
client,
&commands::account::AccountCommand::TradeHistory {
symbol: pair,
limit: limit as u32,
},
)
.await
.map_err(map_anyhow_error)?,
Command::Order(ref cmd) => commands::trade::execute(client, cmd, cli.yes)
.await
.map_err(map_anyhow_error)?,
Command::Withdraw {
asset,
volume,
address,
username,
memo,
network,
callback_url,
} => {
let funding_cmd = commands::funding::FundingCommand::Withdraw {
currency: asset,
amount: volume,
address,
username,
memo,
network,
callback_url,
};
commands::funding::execute(client, config, &funding_cmd, cli.output)
.await
.map_err(map_anyhow_error)?
}
Command::Withdrawal(ref sub) => {
let funding_cmd = match sub {
WithdrawalSubcommand::Fee { asset, network } => {
commands::funding::FundingCommand::WithdrawFee {
currency: asset.clone(),
network: network.clone(),
}
}
#[cfg(feature = "server")]
WithdrawalSubcommand::ServeCallback {
port,
auto_ok,
listen,
} => commands::funding::FundingCommand::ServeCallback {
port: *port,
auto_ok: *auto_ok,
listen: listen.clone(),
},
};
commands::funding::execute(client, config, &funding_cmd, cli.output)
.await
.map_err(map_anyhow_error)?
}
Command::Ws(ref cmd) => commands::websocket::execute(client, cmd, cli.output)
.await
.map_err(map_anyhow_error)?,
Command::Paper(ref cmd) => commands::paper::execute(client, config, cmd)
.await
.map_err(map_anyhow_error)?,
Command::Auth(ref cmd) => commands::auth::execute(client, config, cmd)
.await
.map_err(map_anyhow_error)?,
Command::Alert(ref cmd) => commands::alert::execute(client, &None, cmd)
.await
.map_err(map_anyhow_error)?,
Command::Setup | Command::Shell => {
return Err(IndodaxError::Other(
"This command is handled separately".into(),
));
}
#[cfg(feature = "mcp")]
Command::Mcp { .. } => {
return Err(IndodaxError::Other(
"This command is handled separately".into(),
));
}
};
Ok(output.with_format(cli.output))
}
#[cfg(feature = "cli")]
pub fn map_anyhow_error(e: anyhow::Error) -> IndodaxError {
e.downcast::<IndodaxError>()
.unwrap_or_else(|e| IndodaxError::Other(e.to_string()))
}
#[cfg(all(test, feature = "cli"))]
mod tests {
use super::*;
#[test]
fn test_cli_parse_ticker() {
let args = vec!["indodax", "ticker", "btc_idr"];
let cli = Cli::try_parse_from(args).unwrap();
match cli.command {
Command::Ticker { pair: _ } => {
}
_ => panic!("Expected Ticker command, got {:?}", cli.command),
}
}
#[test]
fn test_cli_parse_output_json() {
let args = vec!["indodax", "-o", "json", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.output, OutputFormat::Json);
}
#[test]
fn test_cli_parse_api_key() {
let args = vec!["indodax", "--api-key", "mykey", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.api_key, Some("mykey".into()));
}
#[test]
fn test_cli_parse_api_secret() {
let args = vec!["indodax", "--api-secret", "mysecret", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.api_secret, Some("mysecret".into()));
}
#[test]
fn test_cli_parse_verbose() {
let args = vec!["indodax", "-v", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert!(cli.verbose);
}
#[test]
fn test_command_variants() {
let _cmd1 = Command::ServerTime;
let _cmd2 = Command::AccountInfo;
let _cmd3 = Command::Order(crate::commands::trade::TradeCommand::Buy {
pair: "btc_idr".into(),
idr: 100_000.0,
price: None,
order_type: None,
});
let _cmd4 = Command::Withdrawal(WithdrawalSubcommand::Fee {
asset: "btc".into(),
network: None,
});
let _cmd5 = Command::Ws(crate::commands::websocket::WebSocketCommand::Ticker {
pair: "btc_idr".into(),
});
let _cmd6 = Command::Paper(crate::commands::paper::PaperCommand::Balance);
let _cmd7 = Command::Auth(crate::commands::auth::AuthCommand::Show);
let _cmd8 = Command::Setup;
let _cmd9 = Command::Shell;
#[cfg(feature = "mcp")]
let _cmd10 = Command::Mcp {
groups: "market,paper".into(),
allow_dangerous: false,
};
}
#[test]
fn test_output_format_clap() {
let args = vec!["indodax", "-o", "table", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.output, OutputFormat::Table);
}
#[test]
fn test_cli_parse_default_output() {
let args = vec!["indodax", "ticker"];
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.output, OutputFormat::Table);
}
#[test]
fn test_command_display() {
let cli = Cli::try_parse_from(vec!["indodax", "ticker"]).unwrap();
let _ = format!("{:?}", cli);
}
}