@ibidathoillah/indodax-cli 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,388 @@
1
+ # indodax-cli
2
+
3
+ Unofficial Rust CLI for Indodax. Use it to inspect markets, manage account data, place spot orders, stream live WebSocket events, run paper trading and price alerts, and expose the same command surface to agents through MCP.
4
+
5
+ [![Rust](https://img.shields.io/badge/Rust-2021-000000?logo=rust)](https://www.rust-lang.org/)
6
+ [![CLI](https://img.shields.io/badge/interface-terminal-2f855a)](#quick-start)
7
+ [![WebSocket](https://img.shields.io/badge/websocket-live-2563eb)](#websocket-streaming)
8
+ [![MCP](https://img.shields.io/badge/MCP-ready-7c3aed)](#mcp-server)
9
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
+
11
+ ## Highlights
12
+
13
+ - Public market data: server time, pairs, ticker, all tickers, summaries, order book, trades, OHLC, and price increments.
14
+ - Private account data: account info, balances, transactions, and trade history.
15
+ - Spot trading: buy, sell, cancel, cancel by client order ID, cancel all, and deadman countdown.
16
+ - Funding: withdrawal fee lookup, crypto withdrawal, and withdrawal callback validation server.
17
+ - Real-time streams: ticker, trades, order book, summary, and private order updates.
18
+ - Paper trading: risk-free simulated trading with balances, orders, fills, history, and status.
19
+ - Price alerts: threshold and percentage alerts with one-shot checks or live WebSocket monitoring.
20
+ - Automation-friendly output: human tables by default, JSON envelopes with `-o json`.
21
+ - Credential resolution: CLI flags, environment variables, or `~/.config/indodax/config.toml`.
22
+ - Agent support: MCP server mode with guarded dangerous operations.
23
+
24
+ ## Recent Highlights
25
+
26
+ - WebSocket reliability overhaul: application-level pings, automatic reconnection with exponential backoff, and private WebSocket support for real-time order and balance updates.
27
+ - Secure WebSocket authentication: configurable WebSocket tokens with fallback to a stable default.
28
+ - TradeAPI-2 compliance: normalized symbol formats and stricter request handling across commands.
29
+ - Response parsing improvements: order book handling supports both legacy and modern API shapes.
30
+
31
+ ## Installation
32
+
33
+ Install from source:
34
+
35
+ ```bash
36
+ git clone https://github.com/ibidathoillah/indodax-cli.git
37
+ cd indodax-cli
38
+ cargo install --path .
39
+ ```
40
+
41
+ Install from crates.io:
42
+
43
+ ```bash
44
+ cargo install indodax-cli
45
+ ```
46
+
47
+ Install from npm:
48
+
49
+ ```bash
50
+ npm install -g indodax-cli
51
+ ```
52
+
53
+ Run with Docker:
54
+
55
+ ```bash
56
+ docker run --rm ibidathoillah/indodax-cli --help
57
+ docker run --rm -v ~/.config/indodax:/root/.config/indodax ibidathoillah/indodax-cli balance
58
+ ```
59
+
60
+ Run from the checkout:
61
+
62
+ ```bash
63
+ cargo build
64
+ ./target/debug/indodax --help
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ Market data does not require credentials:
70
+
71
+ ```bash
72
+ indodax server-time
73
+ indodax ticker btc/idr
74
+ indodax orderbook btc/idr --count 10
75
+ indodax pairs
76
+ indodax ohlc --pair btc/idr
77
+ indodax -o json ticker btc/idr
78
+ ```
79
+
80
+ Configure private API credentials:
81
+
82
+ ```bash
83
+ indodax auth set --api-key YOUR_API_KEY --api-secret YOUR_API_SECRET
84
+ indodax auth test
85
+ indodax auth show
86
+ ```
87
+
88
+ Or use environment variables:
89
+
90
+ ```bash
91
+ export INDODAX_API_KEY=your_api_key
92
+ export INDODAX_API_SECRET=your_api_secret
93
+ ```
94
+
95
+ Credential priority:
96
+
97
+ 1. `--api-key` and `--api-secret`
98
+ 2. `INDODAX_API_KEY` and `INDODAX_API_SECRET`
99
+ 3. `~/.config/indodax/config.toml`
100
+
101
+ ## Command Reference
102
+
103
+ Global options:
104
+
105
+ ```text
106
+ indodax [OPTIONS] <COMMAND>
107
+
108
+ Options:
109
+ -o, --output <table|json> Output format [default: table]
110
+ --api-key <API_KEY> API key override
111
+ --api-secret <API_SECRET> API secret override
112
+ --api-secret-stdin Read API secret from stdin
113
+ -v, --verbose Enable verbose logs
114
+ --yes, --force Skip confirmation prompts
115
+ ```
116
+
117
+ ### Market
118
+
119
+ ```bash
120
+ indodax server-time
121
+ indodax pairs
122
+ indodax ticker btc/idr
123
+ indodax ticker-all
124
+ indodax summaries
125
+ indodax orderbook btc/idr --count 10
126
+ indodax trades btc/idr
127
+ indodax ohlc --pair btc/idr --interval 60
128
+ indodax price-increments
129
+ ```
130
+
131
+ ### Account
132
+
133
+ ```bash
134
+ indodax account-info
135
+ indodax balance
136
+ indodax transactions
137
+ indodax trades-history btc/idr --limit 5
138
+ ```
139
+
140
+ ### Trading
141
+
142
+ ```bash
143
+ indodax order buy --pair btc/idr --idr 100000 --price 1000000000
144
+ indodax order buy --pair btc/idr --idr 100000 --order-type market
145
+ indodax order sell --pair btc/idr --amount 0.001 --price 1000000000
146
+ indodax order cancel --order-id 123456 --pair btc/idr --order-type buy
147
+ indodax order cancel-by-client-id --client-order-id CLIENT_ID
148
+ indodax --yes order cancel-all --pair btc/idr
149
+ indodax order countdown --pair btc/idr --countdown-time 60000
150
+ ```
151
+
152
+ ### Funding
153
+
154
+ ```bash
155
+ indodax withdrawal fee --asset btc
156
+ indodax withdraw --asset btc --volume 0.001 --address bc1... --network BTC
157
+ indodax withdrawal serve-callback --port 8080
158
+ ```
159
+
160
+ For withdrawals, Indodax may require a callback URL. Configure it with:
161
+
162
+ ```bash
163
+ indodax auth set --callback-url https://yourdomain.com/callback
164
+ ```
165
+
166
+ ### WebSocket Streaming
167
+
168
+ Public streams:
169
+
170
+ ```bash
171
+ indodax ws ticker btc/idr
172
+ indodax ws trades btc/idr
173
+ indodax ws book btc/idr
174
+ indodax ws summary
175
+ ```
176
+
177
+ Private stream:
178
+
179
+ ```bash
180
+ indodax ws orders
181
+ ```
182
+
183
+ ### Price Alerts
184
+
185
+ ```bash
186
+ indodax alert add -p btc/idr --above 150000000
187
+ indodax alert add -p btc/idr --below 50000000
188
+ indodax alert add -p btc/idr --percent-up 5
189
+ indodax alert add -p btc/idr --percent-down 10
190
+ indodax alert list
191
+ indodax alert list --history
192
+ indodax alert cancel -i 1
193
+ indodax alert cancel --all
194
+ indodax alert check
195
+ indodax alert watch -p btc/idr
196
+ indodax alert triggered
197
+ ```
198
+
199
+ Alerts are stored in `~/.config/indodax/alerts.json`.
200
+
201
+ ### Paper Trading
202
+
203
+ ```bash
204
+ indodax paper init
205
+ indodax paper init --idr 50000000 --btc 0.5
206
+ indodax paper balance
207
+ indodax paper buy -p btc/idr -i 1000000
208
+ indodax paper buy -p btc/idr -a 0.1 -r 500000000
209
+ indodax paper sell -p btc/idr -a 0.05 -r 1000000000
210
+ indodax paper orders --pair btc/idr
211
+ indodax paper cancel -i 1
212
+ indodax paper cancel-all
213
+ indodax paper fill -i 1
214
+ indodax paper fill -i 2 --price 110000000
215
+ indodax paper fill --all
216
+ indodax paper check-fills -p '{"btc/idr": 95000000, "eth/idr": 12000000}'
217
+ indodax paper topup -c usdt -a 50000
218
+ indodax paper history
219
+ indodax paper status
220
+ indodax paper reset
221
+ ```
222
+
223
+ Paper trading mirrors the live order interface for safer strategy testing.
224
+
225
+ ### Interactive Shell
226
+
227
+ ```bash
228
+ indodax shell
229
+ ```
230
+
231
+ ### MCP Server
232
+
233
+ ```bash
234
+ indodax mcp
235
+ indodax mcp -s all
236
+ indodax mcp -s all --allow-dangerous
237
+ indodax mcp -s market,trade,paper
238
+ ```
239
+
240
+ Service groups:
241
+
242
+ | Group | Tools | Auth Required | Dangerous |
243
+ |-------|-------|---------------|-----------|
244
+ | `market` | Server time, ticker, pairs, orderbook, trades, OHLC, price increments | No | No |
245
+ | `account` | Balance, trade history, transactions, account info | Yes | No |
246
+ | `trade` | Buy, sell, cancel orders | Yes | Yes |
247
+ | `funding` | Withdraw fees, withdraw crypto | Yes | Yes |
248
+ | `paper` | Paper trading init, balance, buy, sell, orders, cancel, history, status | No | No |
249
+ | `auth` | Show config, test credentials | Varies | No |
250
+
251
+ By default, `trade` and `funding` MCP tools require `acknowledged: true`. Use `--allow-dangerous` only for controlled local automation.
252
+
253
+ Example MCP client configuration:
254
+
255
+ ```json
256
+ {
257
+ "mcpServers": {
258
+ "indodax": {
259
+ "command": "indodax",
260
+ "args": ["mcp", "-s", "all"]
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ ## Output Formats
267
+
268
+ Table mode is the default:
269
+
270
+ ```bash
271
+ indodax ticker btc/idr
272
+ ```
273
+
274
+ JSON mode is intended for scripting and automation:
275
+
276
+ ```bash
277
+ indodax -o json ticker btc/idr
278
+ ```
279
+
280
+ Error responses in JSON mode use structured envelopes:
281
+
282
+ ```json
283
+ {
284
+ "error": true,
285
+ "message": "Invalid trading pair: xxx_idr",
286
+ "error_type": "invalid_pair",
287
+ "retryable": false
288
+ }
289
+ ```
290
+
291
+ ## E2E Testing
292
+
293
+ The repository includes live API smoke tests:
294
+
295
+ ```bash
296
+ ./scripts/e2e_minimal.sh --public
297
+ ./scripts/e2e_minimal.sh --private
298
+ ./scripts/e2e_minimal.sh --ws
299
+ ./scripts/e2e_websocket_mock.sh
300
+ ```
301
+
302
+ Environment knobs:
303
+
304
+ ```bash
305
+ INDODAX_BIN=./target/debug/indodax
306
+ ```
307
+
308
+ Latest local verification:
309
+
310
+ ```text
311
+ cargo test: 296 passed
312
+ ./scripts/e2e_minimal.sh --public: passed
313
+ ./scripts/e2e_minimal.sh --private: skipped private checks (credentials unavailable)
314
+ ./scripts/e2e_minimal.sh --ws: passed
315
+ ```
316
+
317
+ ## API Coverage
318
+
319
+ - Public REST: Indodax market endpoints
320
+ - Private REST: Indodax TradeAPI and TradeAPI-2 endpoints
321
+ - Public WebSocket: `wss://ws3.indodax.com/ws/`
322
+ - Private WebSocket: `wss://pws.indodax.com/ws/`
323
+
324
+ ## Architecture
325
+
326
+ This project is inspired by the Kraken CLI architecture and built with modern Rust:
327
+
328
+ - `clap` for derive-based CLI parsing
329
+ - `tokio` for async runtime
330
+ - `tokio-tungstenite` for WebSocket streams
331
+ - `reqwest` for REST API calls
332
+ - `serde` for serialization and deserialization
333
+ - `comfy-table` for terminal tables
334
+ - `rmcp` for Model Context Protocol support
335
+
336
+ ## Testing Standards
337
+
338
+ Every release should pass:
339
+
340
+ - Unit tests with `cargo test`
341
+ - Public E2E smoke tests
342
+ - WebSocket smoke tests
343
+ - Private read-only tests when credentials are available
344
+
345
+ Coverage is maintained across `auth`, `client`, `config`, `errors`, `commands`, `mcp`, and `output` modules.
346
+
347
+ ## Security
348
+
349
+ - Credentials are stored with `0600` permissions when using `indodax auth set`.
350
+ - HMAC-SHA512 signing is used for private API authentication.
351
+ - Prefer read-only API keys for account inspection and WebSocket monitoring.
352
+ - Use least-privilege exchange API keys for MCP and automation.
353
+ - Never commit real API keys, secrets, callback tokens, or listen keys.
354
+
355
+ ## Development
356
+
357
+ ```bash
358
+ cargo fmt
359
+ cargo test
360
+ cargo build
361
+ ```
362
+
363
+ ## Contributing
364
+
365
+ Contributions are welcome:
366
+
367
+ 1. Fork the repository.
368
+ 2. Create a feature branch.
369
+ 3. Run tests and relevant E2E smoke checks.
370
+ 4. Open a pull request.
371
+
372
+ ## Related Projects
373
+
374
+ If you use multiple exchanges, check out these related CLI tools built with the same architecture:
375
+
376
+ - [indodax-cli](https://github.com/ibidathoillah/indodax-cli) - CLI for Indodax
377
+ - [bittime-cli](https://github.com/ibidathoillah/bittime-cli) - CLI for Bittime
378
+ - [binance-cli](https://github.com/ibidathoillah/binance-cli) - CLI for Binance Spot
379
+ - [tokocrypto-cli](https://github.com/ibidathoillah/tokocrypto-cli) - CLI for Tokocrypto
380
+ - [kraken-cli](https://github.com/ibidathoillah/kraken-cli) - CLI for Kraken (Spot, Margin, Futures)
381
+
382
+ ## License
383
+
384
+ MIT
385
+
386
+ ## Disclaimer
387
+
388
+ This project is unofficial and is not affiliated with or endorsed by Indodax. Cryptocurrency trading is risky; review commands carefully before using write-capable API keys.
package/bin/indodax.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ // Determine the binary name based on platform
8
+ const binName = process.platform === 'win32' ? 'indodax-native.exe' : 'indodax-native';
9
+ const binPath = path.join(__dirname, binName);
10
+
11
+ // Check if the binary exists
12
+ if (!fs.existsSync(binPath)) {
13
+ // Fallback: Check if it's named 'indodax' (old naming)
14
+ const oldBinName = process.platform === 'win32' ? 'indodax.exe' : 'indodax';
15
+ const oldBinPath = path.join(__dirname, oldBinName);
16
+
17
+ if (fs.existsSync(oldBinPath)) {
18
+ runBinary(oldBinPath);
19
+ } else {
20
+ console.error(`\x1b[31mError: Indodax native binary not found.\x1b[0m`);
21
+ console.error(`Expected at: ${binPath}`);
22
+ console.error(`\nThis usually happens if the post-install download failed.`);
23
+ console.error(`You can try to:`);
24
+ console.error(`1. Reinstall: npm install -g indodax-cli`);
25
+ console.error(`2. Build from source: cargo install --path .`);
26
+ process.exit(1);
27
+ }
28
+ } else {
29
+ runBinary(binPath);
30
+ }
31
+
32
+ function runBinary(path) {
33
+ const child = spawn(path, process.argv.slice(2), {
34
+ stdio: 'inherit'
35
+ });
36
+
37
+ child.on('exit', (code) => {
38
+ process.exit(code || 0);
39
+ });
40
+
41
+ child.on('error', (err) => {
42
+ console.error(`\x1b[31mError spawning binary:\x1b[0m ${err.message}`);
43
+ process.exit(1);
44
+ });
45
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ibidathoillah/indodax-cli",
3
+ "version": "0.1.21",
4
+ "description": "Command-line interface for the Indodax cryptocurrency exchange",
5
+ "bin": {
6
+ "indodax": "./bin/indodax.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node scripts/install.js"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ibidathoillah/indodax-cli.git"
14
+ },
15
+ "keywords": [
16
+ "indodax",
17
+ "crypto",
18
+ "cli",
19
+ "trading",
20
+ "bitcoin"
21
+ ],
22
+ "author": "Ibida Thoillah",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/ibidathoillah/indodax-cli/issues"
26
+ },
27
+ "homepage": "https://github.com/ibidathoillah/indodax-cli#readme",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
package/pr24.diff ADDED
@@ -0,0 +1,215 @@
1
+ diff --git a/src/commands/utility.rs b/src/commands/utility.rs
2
+ index 20c2d76..e03c716 100644
3
+ --- a/src/commands/utility.rs
4
+ +++ b/src/commands/utility.rs
5
+ @@ -89,7 +89,7 @@ async fn shell() -> Result<CommandOutput> {
6
+ Ok(input) => {
7
+ let _ = rl.add_history_entry(&input);
8
+ let args = format!("indodax {}", input);
9
+ - let args: Vec<&str> = shell_parse(&args);
10
+ + let args: Vec<String> = shell_parse(&args);
11
+ match Cli::try_parse_from(args) {
12
+ Ok(cli) => {
13
+ if matches!(cli.command, crate::Command::Shell) {
14
+ @@ -119,30 +119,44 @@ async fn shell() -> Result<CommandOutput> {
15
+ Ok(CommandOutput::json(data))
16
+ }
17
+
18
+ -fn shell_parse(input: &str) -> Vec<&str> {
19
+ - let mut parts = Vec::new();
20
+ - let mut current = "";
21
+ +/// Splits a shell-style command line into argv-like tokens.
22
+ +fn shell_parse(input: &str) -> Vec<String> {
23
+ + let mut parts: Vec<String> = Vec::new();
24
+ + let mut current = String::new();
25
+ let mut in_quote = false;
26
+ + let mut has_token = false;
27
+ + let mut chars = input.chars().peekable();
28
+
29
+ - for word in input.split(' ') {
30
+ - if in_quote {
31
+ - if word.ends_with('"') {
32
+ - in_quote = false;
33
+ - parts.push(&input[current.len() + 1..current.len() + 1 + parts.last().map(|s: &&str| s.len()).unwrap_or(0) + word.len() - 1]);
34
+ + while let Some(ch) = chars.next() {
35
+ + match ch {
36
+ + '"' => {
37
+ + in_quote = !in_quote;
38
+ + has_token = true;
39
+ + }
40
+ + '\\' if in_quote => match chars.peek() {
41
+ + Some('"') | Some('\\') => {
42
+ + current.push(chars.next().unwrap());
43
+ + }
44
+ + _ => current.push(ch),
45
+ + },
46
+ + c if c.is_whitespace() && !in_quote => {
47
+ + if has_token {
48
+ + parts.push(std::mem::take(&mut current));
49
+ + has_token = false;
50
+ + }
51
+ + }
52
+ + c => {
53
+ + current.push(c);
54
+ + has_token = true;
55
+ }
56
+ - } else if word.starts_with('"') {
57
+ - in_quote = true;
58
+ - current = word;
59
+ - } else {
60
+ - parts.push(word);
61
+ }
62
+ }
63
+
64
+ - if parts.is_empty() {
65
+ - input.split_whitespace().collect()
66
+ - } else {
67
+ - parts
68
+ + if has_token {
69
+ + parts.push(current);
70
+ }
71
+ +
72
+ + parts
73
+ }
74
+
75
+ #[cfg(test)]
76
+ @@ -151,67 +165,109 @@ mod tests {
77
+
78
+ #[test]
79
+ fn test_shell_parse_simple() {
80
+ - let input = "market ticker btc_idr";
81
+ - let result = shell_parse(input);
82
+ + let result = shell_parse("market ticker btc_idr");
83
+ assert_eq!(result, vec!["market", "ticker", "btc_idr"]);
84
+ }
85
+
86
+ #[test]
87
+ fn test_shell_parse_single_word() {
88
+ - let input = "help";
89
+ - let result = shell_parse(input);
90
+ + let result = shell_parse("help");
91
+ assert_eq!(result, vec!["help"]);
92
+ }
93
+
94
+ #[test]
95
+ fn test_shell_parse_empty() {
96
+ - let input = "";
97
+ - let result = shell_parse(input);
98
+ - assert!(result.is_empty() || result == vec![""]);
99
+ + let result = shell_parse("");
100
+ + assert!(result.is_empty());
101
+ }
102
+
103
+ #[test]
104
+ fn test_shell_parse_with_quotes() {
105
+ - let input = r#"auth set --api-key "my key" --api-secret "my secret""#;
106
+ - let result = shell_parse(input);
107
+ - // The shell_parse function doesn't handle quotes like this perfectly,
108
+ - // but let's test what it actually does
109
+ - assert!(!result.is_empty());
110
+ + let result =
111
+ + shell_parse(r#"auth set --api-key "my key" --api-secret "my secret""#);
112
+ + assert_eq!(
113
+ + result,
114
+ + vec![
115
+ + "auth", "set", "--api-key", "my key", "--api-secret", "my secret",
116
+ + ]
117
+ + );
118
+ + }
119
+ +
120
+ + #[test]
121
+ + fn test_shell_parse_quoted_value_with_dash() {
122
+ + let result = shell_parse(r#"market ticker --pair "btc_idr""#);
123
+ + assert_eq!(result, vec!["market", "ticker", "--pair", "btc_idr"]);
124
+ }
125
+
126
+ #[test]
127
+ fn test_shell_parse_multiple_spaces() {
128
+ - let input = "market ticker btc_idr";
129
+ - let result = shell_parse(input);
130
+ - // The function doesn't normalize multiple spaces perfectly
131
+ - assert!(result.contains(&"market") || result.len() >= 3);
132
+ + let result = shell_parse("market ticker btc_idr");
133
+ + assert_eq!(result, vec!["market", "ticker", "btc_idr"]);
134
+ }
135
+
136
+ #[test]
137
+ fn test_shell_parse_leading_trailing_spaces() {
138
+ - let input = " market ticker btc_idr ";
139
+ - let result = shell_parse(input);
140
+ - assert!(result.contains(&"market") || result.len() >= 3);
141
+ + let result = shell_parse(" market ticker btc_idr ");
142
+ + assert_eq!(result, vec!["market", "ticker", "btc_idr"]);
143
+ }
144
+
145
+ #[test]
146
+ - fn test_utility_command_variants() {
147
+ - let _cmd1 = UtilityCommand::Setup;
148
+ - let _cmd2 = UtilityCommand::Shell;
149
+ + fn test_shell_parse_only_whitespace() {
150
+ + let result = shell_parse(" ");
151
+ + assert!(result.is_empty());
152
+ + }
153
+ +
154
+ + #[test]
155
+ + fn test_shell_parse_quoted_empty_string() {
156
+ + let result = shell_parse(r#"set key """#);
157
+ + assert_eq!(result, vec!["set", "key", ""]);
158
+ + }
159
+ +
160
+ + #[test]
161
+ + fn test_shell_parse_quoted_whitespace_only() {
162
+ + let result = shell_parse(r#"echo " ""#);
163
+ + assert_eq!(result, vec!["echo", " "]);
164
+ }
165
+
166
+ #[test]
167
+ - fn test_shell_parse_whitespace_fallback() {
168
+ - // Test the fallback path when parts is empty
169
+ - let input = "simple";
170
+ - let result = shell_parse(input);
171
+ - assert_eq!(result.len(), 1);
172
+ + fn test_shell_parse_escaped_quote_inside_quotes() {
173
+ + let result = shell_parse(r#"echo "he said \"hi\"""#);
174
+ + assert_eq!(result, vec!["echo", r#"he said "hi""#]);
175
+ + }
176
+ +
177
+ + #[test]
178
+ + fn test_shell_parse_escaped_backslash_inside_quotes() {
179
+ + let result = shell_parse(r#"path "a\\b""#);
180
+ + assert_eq!(result, vec!["path", r#"a\b"#]);
181
+ + }
182
+ +
183
+ + #[test]
184
+ + fn test_shell_parse_unclosed_quote_keeps_token() {
185
+ + let result = shell_parse(r#"foo "bar baz"#);
186
+ + assert_eq!(result, vec!["foo", "bar baz"]);
187
+ + }
188
+ +
189
+ + #[test]
190
+ + fn test_shell_parse_adjacent_quoted_and_bare() {
191
+ + let result = shell_parse(r#"x="hello world""#);
192
+ + assert_eq!(result, vec!["x=hello world"]);
193
+ + }
194
+ +
195
+ + #[test]
196
+ + fn test_shell_parse_tab_separator() {
197
+ + let result = shell_parse("a\tb\tc");
198
+ + assert_eq!(result, vec!["a", "b", "c"]);
199
+ + }
200
+ +
201
+ + #[test]
202
+ + fn test_utility_command_variants() {
203
+ + let _cmd1 = UtilityCommand::Setup;
204
+ + let _cmd2 = UtilityCommand::Shell;
205
+ }
206
+
207
+ #[test]
208
+ fn test_shell_parse_with_dash_args() {
209
+ - let input = "account balance -v";
210
+ - let result = shell_parse(input);
211
+ - assert!(result.contains(&"account") || result.len() >= 2);
212
+ + let result = shell_parse("account balance -v");
213
+ + assert_eq!(result, vec!["account", "balance", "-v"]);
214
+ }
215
+ }
@@ -0,0 +1,202 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ packages: write
10
+ contents: write
11
+
12
+ jobs:
13
+ build-and-release:
14
+ name: Build and Release
15
+ runs-on: ${{ matrix.os }}
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ include:
20
+ - os: ubuntu-latest
21
+ target: x86_64-unknown-linux-gnu
22
+ artifact_name: PROJECT_NAME-linux-x86_64
23
+ binary_extension: ""
24
+ - os: ubuntu-latest
25
+ target: aarch64-unknown-linux-gnu
26
+ artifact_name: PROJECT_NAME-linux-aarch64
27
+ binary_extension: ""
28
+ - os: macos-latest
29
+ target: x86_64-apple-darwin
30
+ artifact_name: PROJECT_NAME-macos-x86_64
31
+ binary_extension: ""
32
+ - os: macos-latest
33
+ target: aarch64-apple-darwin
34
+ artifact_name: PROJECT_NAME-macos-aarch64
35
+ binary_extension: ""
36
+ - os: windows-latest
37
+ target: x86_64-pc-windows-msvc
38
+ artifact_name: PROJECT_NAME-windows-x86_64
39
+ binary_extension: ".exe"
40
+
41
+ steps:
42
+ - name: Checkout
43
+ uses: actions/checkout@v4
44
+
45
+ - name: Install Rust
46
+ uses: dtolnay/rust-toolchain@stable
47
+ with:
48
+ targets: ${{ matrix.target }}
49
+
50
+ - name: Install cross-compilation tools (Linux ARM64)
51
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
52
+ run: |
53
+ sudo apt-get update
54
+ sudo apt-get install -y gcc-aarch64-linux-gnu
55
+
56
+ - name: Build
57
+ shell: bash
58
+ run: |
59
+ if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
60
+ export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
61
+ fi
62
+
63
+ cargo build --release --target ${{ matrix.target }}
64
+
65
+ BIN_DIR="target/${{ matrix.target }}/release"
66
+ cp "$BIN_DIR/PROJECT_NAME${{ matrix.binary_extension }}" "${{ matrix.artifact_name }}${{ matrix.binary_extension }}"
67
+
68
+ - name: Upload Artifact
69
+ uses: actions/upload-artifact@v4
70
+ with:
71
+ name: ${{ matrix.artifact_name }}
72
+ path: ${{ matrix.artifact_name }}${{ matrix.binary_extension }}
73
+
74
+ publish-cargo:
75
+ name: Publish to Cargo
76
+ needs: build-and-release
77
+ runs-on: ubuntu-latest
78
+ steps:
79
+ - name: Checkout
80
+ uses: actions/checkout@v4
81
+ - name: Install Rust
82
+ uses: dtolnay/rust-toolchain@stable
83
+ - name: Publish
84
+ env:
85
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
86
+ run: cargo publish --token $CARGO_REGISTRY_TOKEN --no-verify --allow-dirty || echo "Already published"
87
+
88
+ publish-npm:
89
+ name: Publish to NPM
90
+ needs: build-and-release
91
+ runs-on: ubuntu-latest
92
+ steps:
93
+ - name: Checkout
94
+ uses: actions/checkout@v4
95
+ - name: Setup Node
96
+ uses: actions/setup-node@v4
97
+ with:
98
+ node-version: '20'
99
+ registry-url: 'https://registry.npmjs.org'
100
+ - name: NPM_EXTRA_STEPS
101
+ - name: Publish
102
+ env:
103
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
104
+ run: |
105
+ NPM_PUBLISH_COMMAND
106
+
107
+ publish-docker:
108
+ name: Publish to Docker Hub
109
+ needs: build-and-release
110
+ runs-on: ubuntu-latest
111
+ steps:
112
+ - name: Checkout
113
+ uses: actions/checkout@v4
114
+ - name: Log in to Docker Hub
115
+ uses: docker/login-action@v3
116
+ with:
117
+ username: ${{ secrets.DOCKER_USERNAME }}
118
+ password: ${{ secrets.DOCKER_PASSWORD }}
119
+ - name: Build and Push Docker Image
120
+ uses: docker/build-push-action@v5
121
+ with:
122
+ context: .
123
+ file: ./Dockerfile
124
+ push: true
125
+ tags: |
126
+ ${{ secrets.DOCKER_USERNAME }}/PROJECT_NAME-cli:${{ github.ref_name }}
127
+ ${{ secrets.DOCKER_USERNAME }}/PROJECT_NAME-cli:latest
128
+
129
+ publish-gpr:
130
+ name: Publish to GitHub Packages (NPM)
131
+ needs: build-and-release
132
+ runs-on: ubuntu-latest
133
+ permissions:
134
+ contents: read
135
+ packages: write
136
+ steps:
137
+ - name: Checkout
138
+ uses: actions/checkout@v4
139
+ - name: Setup Node
140
+ uses: actions/setup-node@v4
141
+ with:
142
+ node-version: "20"
143
+ registry-url: "https://npm.pkg.github.com"
144
+ scope: "@ibidathoillah"
145
+ - name: Publish to GPR
146
+ env:
147
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
148
+ run: |
149
+ sed -i 's|https://registry.npmjs.org|https://npm.pkg.github.com|g' package.json || true
150
+ npm publish || echo "Already published to GPR"
151
+
152
+ publish-ghcr:
153
+ name: Publish to GHCR
154
+ needs: build-and-release
155
+ runs-on: ubuntu-latest
156
+ permissions:
157
+ contents: read
158
+ packages: write
159
+ steps:
160
+ - name: Checkout
161
+ uses: actions/checkout@v4
162
+ - name: Log in to GHCR
163
+ uses: docker/login-action@v3
164
+ with:
165
+ registry: ghcr.io
166
+ username: ${{ github.actor }}
167
+ password: ${{ secrets.GITHUB_TOKEN }}
168
+ - name: Build and Push to GHCR
169
+ uses: docker/build-push-action@v5
170
+ with:
171
+ context: .
172
+ file: ./Dockerfile
173
+ push: true
174
+ tags: |
175
+ ghcr.io/${{ github.repository }}:${{ github.ref_name }}
176
+ ghcr.io/${{ github.repository }}:latest
177
+
178
+ github-release:
179
+ name: GitHub Release
180
+ needs: build-and-release
181
+ runs-on: ubuntu-latest
182
+ steps:
183
+ - name: Checkout
184
+ uses: actions/checkout@v4
185
+ - name: Download Artifacts
186
+ uses: actions/download-artifact@v4
187
+ with:
188
+ merge-multiple: true
189
+
190
+ - name: Create Release
191
+ uses: softprops/action-gh-release@v2
192
+ with:
193
+ files: |
194
+ PROJECT_NAME-linux-x86_64
195
+ PROJECT_NAME-linux-aarch64
196
+ PROJECT_NAME-macos-x86_64
197
+ PROJECT_NAME-macos-aarch64
198
+ PROJECT_NAME-windows-x86_64.exe
199
+ body_path: RELEASE_NOTES.md
200
+ fail_on_unmatched_files: false
201
+ env:
202
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # Minimal Balance E2E Test Script for Indodax CLI
3
+ # This script performs safe, read-only and minimal-amount operations to verify CLI health.
4
+
5
+ set -e
6
+
7
+ echo "--- [E2E] Starting Minimal Risk Tests ---"
8
+
9
+ RUN_PUBLIC=true
10
+ RUN_PRIVATE=true
11
+ RUN_WS=true
12
+
13
+ if [[ "${1:-}" == "--public" ]]; then
14
+ RUN_PRIVATE=false
15
+ RUN_WS=false
16
+ elif [[ "${1:-}" == "--private" ]]; then
17
+ RUN_PUBLIC=false
18
+ RUN_WS=false
19
+ elif [[ "${1:-}" == "--ws" ]]; then
20
+ RUN_PUBLIC=false
21
+ RUN_PRIVATE=false
22
+ fi
23
+
24
+ INDODAX="${INDODAX_BIN:-./target/debug/indodax}"
25
+ PAIR="${INDODAX_TEST_PAIR:-btc_idr}"
26
+ PAIR_FLEX="${INDODAX_TEST_PAIR_FLEX:-btc/idr}"
27
+ if [ ! -x "$INDODAX" ]; then
28
+ INDODAX="./target/release/indodax"
29
+ fi
30
+
31
+ if [ ! -x "$INDODAX" ]; then
32
+ echo "Building indodax-cli ..."
33
+ cargo build
34
+ INDODAX="./target/debug/indodax"
35
+ fi
36
+
37
+ # Function to check that a command succeeds and returns output.
38
+ check_not_empty() {
39
+ local cmd=$1
40
+ local name=$2
41
+ echo "Testing: $name..."
42
+ local output
43
+ output=$($INDODAX $cmd)
44
+
45
+ if [ -n "$output" ]; then
46
+ echo "✅ $name: OK"
47
+ else
48
+ echo "❌ $name: FAILED (Empty output)"
49
+ echo "$output"
50
+ exit 1
51
+ fi
52
+ }
53
+
54
+ if $RUN_PUBLIC; then
55
+ # 1. Public API Verification
56
+ echo "[1/5] Checking Public Market Data..."
57
+ check_not_empty "ticker $PAIR" "Market Ticker"
58
+ check_not_empty "pairs" "Market Pairs"
59
+ check_not_empty "trades $PAIR_FLEX" "Market Trades"
60
+ check_not_empty "ohlc --pair $PAIR_FLEX" "Market OHLC"
61
+ echo "✅ Public Market Data OK"
62
+ fi
63
+
64
+ if $RUN_PRIVATE; then
65
+ if $INDODAX auth test >/dev/null 2>&1; then
66
+ # 2. Authentication & V1 API Verification (Read-Only)
67
+ echo "[2/5] Checking Private V1 API (Balance)..."
68
+ check_not_empty "balance" "Account Balance"
69
+ echo "✅ Private V1 (Auth) OK"
70
+
71
+ # 3. API V2 Verification (Read-Only)
72
+ echo "[3/5] Checking Private V2 API (Order History)..."
73
+ # Note: History might be empty for new accounts, so we check for no error
74
+ $INDODAX trades-history "$PAIR_FLEX" --limit 1 > /dev/null
75
+ echo "✅ Private V2 (History) OK"
76
+ else
77
+ echo "[2/5] Private API: SKIP (credentials unavailable or invalid)"
78
+ echo "[3/5] Private V2 API: SKIP (credentials unavailable or invalid)"
79
+ fi
80
+ fi
81
+
82
+ if $RUN_WS; then
83
+ # 4. WebSocket Connectivity (Brief check)
84
+ echo "[4/5] Checking WebSocket Connectivity (5s)..."
85
+ ws_output=$(timeout 5s $INDODAX --output json ws ticker "$PAIR_FLEX" 2>&1 || true)
86
+ if echo "$ws_output" | grep -q "connecting"; then
87
+ echo "✅ WebSocket: OK (connected)"
88
+ else
89
+ echo "❌ WebSocket: FAILED"
90
+ echo "$ws_output"
91
+ exit 1
92
+ fi
93
+ fi
94
+
95
+ # 5. Minimal Trade Verification (Optional/Manual)
96
+ echo "[5/5] Minimal Trade Verification (Instructions):"
97
+ echo " To verify execution, run: indodax order buy --pair $PAIR_FLEX --idr 10000"
98
+ echo " Then immediately cancel: indodax --yes order cancel-all --pair $PAIR_FLEX"
99
+
100
+ echo "--- [E2E] Minimal Tests Completed Successfully ---"
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # E2E Mock verification for WebSocket commands
3
+ # This script runs public websocket commands for a few seconds and verifies they produce output.
4
+
5
+ set -e
6
+
7
+ echo "Starting WebSocket E2E verification..."
8
+
9
+ PAIR="${INDODAX_TEST_PAIR:-btc_idr}"
10
+ PAIR_FLEX="${INDODAX_TEST_PAIR_FLEX:-btc/idr}"
11
+
12
+ # Function to test a websocket command
13
+ test_ws() {
14
+ local cmd=$1
15
+ local name=$2
16
+ echo "Testing: $name..."
17
+
18
+ # Run command with timeout. It should produce at least one event in 10 seconds for BTC_IDR
19
+ # We use --output json to make it easy to verify.
20
+ output=$(timeout 15s "$INDODAX" --output json ws $cmd 2>&1 || true)
21
+
22
+ if echo "$output" | grep -q "\"event\""; then
23
+ echo "✅ $name: Success (received events)"
24
+ elif echo "$output" | grep -q "connecting"; then
25
+ echo "✅ $name: Success (connected but no events in timeout window)"
26
+ else
27
+ echo "❌ $name: Failed (no connection or events)"
28
+ echo "Debug output: $output"
29
+ return 1
30
+ fi
31
+ }
32
+
33
+ INDODAX="${INDODAX_BIN:-./target/debug/indodax}"
34
+ if [ ! -x "$INDODAX" ]; then
35
+ echo "Building indodax-cli ..."
36
+ cargo build
37
+ fi
38
+
39
+ if [ ! -x "$INDODAX" ]; then
40
+ echo "Error: $INDODAX not found after cargo build."
41
+ exit 1
42
+ fi
43
+
44
+ test_ws "ticker $PAIR_FLEX" "Public Ticker"
45
+ test_ws "summary" "Public Summary"
46
+ test_ws "book $PAIR" "Public Orderbook"
47
+
48
+ echo "E2E verification complete."
@@ -0,0 +1,87 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
4
+ const os = require('os');
5
+
6
+ // Get version from package.json
7
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
8
+ const VERSION = pkg.version;
9
+ const REPO = 'ibidathoillah/indodax-cli';
10
+
11
+ function getBinaryUrl() {
12
+ const platform = os.platform();
13
+ const arch = os.arch();
14
+
15
+ let osName = '';
16
+ let archName = '';
17
+
18
+ if (platform === 'linux') {
19
+ osName = 'linux';
20
+ } else if (platform === 'darwin') {
21
+ osName = 'macos';
22
+ } else if (platform === 'win32') {
23
+ osName = 'windows';
24
+ } else {
25
+ console.error(`Unsupported platform: ${platform}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ if (arch === 'x64') {
30
+ archName = 'x86_64';
31
+ } else if (arch === 'arm64') {
32
+ archName = 'aarch64';
33
+ } else {
34
+ console.error(`Unsupported architecture: ${arch}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const ext = platform === 'win32' ? '.exe' : '';
39
+ return `https://github.com/${REPO}/releases/download/v${VERSION}/indodax-${osName}-${archName}${ext}`;
40
+ }
41
+
42
+ const binDir = path.join(__dirname, '..', 'bin');
43
+ const binName = os.platform() === 'win32' ? 'indodax.exe' : 'indodax';
44
+ const binPath = path.join(binDir, binName);
45
+
46
+ if (!fs.existsSync(binDir)) {
47
+ fs.mkdirSync(binDir, { recursive: true });
48
+ }
49
+
50
+ function download(url, dest) {
51
+ console.log(`Downloading indodax-cli binary from ${url}...`);
52
+
53
+ const file = fs.createWriteStream(dest);
54
+
55
+ https.get(url, (response) => {
56
+ if (response.statusCode === 301 || response.statusCode === 302) {
57
+ download(response.headers.location, dest);
58
+ return;
59
+ }
60
+
61
+ if (response.statusCode !== 200) {
62
+ console.warn(`\x1b[33mWarning:\x1b[0m Server returned status code ${response.statusCode}`);
63
+ if (response.statusCode === 404) {
64
+ console.warn(`Binary for version v${VERSION} not found on GitHub releases.`);
65
+ console.warn(`The command 'indodax' will still be registered, but you may need to build it manually.`);
66
+ }
67
+ fs.unlink(dest, () => { });
68
+ // Exit with 0 to allow the npm installation to complete
69
+ process.exit(0);
70
+ }
71
+
72
+ response.pipe(file);
73
+
74
+ file.on('finish', () => {
75
+ file.close();
76
+ fs.chmodSync(dest, 0o755);
77
+ console.log('\x1b[32mindodax-cli binary installed successfully.\x1b[0m');
78
+ });
79
+ }).on('error', (err) => {
80
+ fs.unlink(dest, () => { });
81
+ console.error(`\x1b[31mError downloading binary:\x1b[0m ${err.message}`);
82
+ process.exit(1);
83
+ });
84
+ }
85
+
86
+ const url = getBinaryUrl();
87
+ download(url, binPath);
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # scripts/release.sh - Automate Indodax CLI releases
3
+ # Usage: ./scripts/release.sh [version]
4
+ # If version is omitted, it reads from Cargo.toml
5
+
6
+ set -e
7
+
8
+ # 1. Get current version from Cargo.toml if not provided
9
+ if [ -z "$1" ]; then
10
+ VERSION=$(grep '^version =' Cargo.toml | head -n1 | cut -d'"' -f2)
11
+ echo "Using version $VERSION from Cargo.toml"
12
+ else
13
+ VERSION=$1
14
+ echo "Setting version to $VERSION"
15
+ # Update Cargo.toml
16
+ sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
17
+ fi
18
+
19
+ # 2. Sync package.json
20
+ if [ -f "package.json" ]; then
21
+ sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json
22
+ fi
23
+
24
+ # 3. Update RELEASE_NOTES.md headers
25
+ if [ -f "RELEASE_NOTES.md" ]; then
26
+ sed -i "s/Welcome to Indodax CLI v[0-9.]*/Welcome to Indodax CLI v$VERSION/" RELEASE_NOTES.md
27
+ sed -i "s/What's New in v[0-9.]*/What's New in v$VERSION/" RELEASE_NOTES.md
28
+ fi
29
+
30
+ # 4. Update README.md highlights
31
+ if [ -f "README.md" ]; then
32
+ sed -i "s/Recent Highlights (v[0-9.]*)/Recent Highlights (v$VERSION)/" README.md
33
+ fi
34
+
35
+ # 5. Update TODO.md
36
+ if [ -f "TODO.md" ]; then
37
+ sed -i "s/WebSocket Reliability Overhaul (v[0-9.]*)/WebSocket Reliability Overhaul (v$VERSION)/" TODO.md
38
+ fi
39
+
40
+ echo "✅ All files updated to v$VERSION"
41
+
42
+ # 6. Git Operations (Optional - uncomment if you want auto-commit)
43
+ # git add Cargo.toml package.json RELEASE_NOTES.md README.md TODO.md
44
+ # git commit -m "chore: release v$VERSION"
45
+ # git push origin main
46
+ # git tag -f v$VERSION
47
+ # git push origin v$VERSION -f
48
+
49
+ # 7. GitHub Release
50
+ if command -v gh &> /dev/null; then
51
+ echo "Updating GitHub release v$VERSION..."
52
+ gh release edit "v$VERSION" --notes-file RELEASE_NOTES.md || \
53
+ gh release create "v$VERSION" --title "v$VERSION" --notes-file RELEASE_NOTES.md
54
+ fi
55
+
56
+ echo "🚀 Release v$VERSION completed successfully!"