@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 +388 -0
- package/bin/indodax.js +45 -0
- package/package.json +31 -0
- package/pr24.diff +215 -0
- package/release_template.yml.tmp +202 -0
- package/scripts/e2e_minimal.sh +100 -0
- package/scripts/e2e_websocket_mock.sh +48 -0
- package/scripts/install.js +87 -0
- package/scripts/release.sh +56 -0
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
|
+
[](https://www.rust-lang.org/)
|
|
6
|
+
[](#quick-start)
|
|
7
|
+
[](#websocket-streaming)
|
|
8
|
+
[](#mcp-server)
|
|
9
|
+
[](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!"
|