perp-cli 0.3.3
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial / Security Tests — "Ethical Hacker" Mode
|
|
3
|
+
*
|
|
4
|
+
* Tests what happens when a malicious or careless human/agent tries to:
|
|
5
|
+
* 1. Inject shell commands via arguments
|
|
6
|
+
* 2. Pass extreme/malformed values (NaN, Infinity, negative, huge)
|
|
7
|
+
* 3. Use special chars, unicode, path traversal in symbols
|
|
8
|
+
* 4. Leak private keys via error messages
|
|
9
|
+
* 5. Break JSON envelope with crafted inputs
|
|
10
|
+
* 6. Exploit prototype pollution or type confusion
|
|
11
|
+
* 7. Exhaust resources with huge limits
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import { registerTradeCommands } from "../commands/trade.js";
|
|
16
|
+
import { registerMarketCommands } from "../commands/market.js";
|
|
17
|
+
import { registerAccountCommands } from "../commands/account.js";
|
|
18
|
+
import { classifyError, PerpError } from "../errors.js";
|
|
19
|
+
import { jsonOk, jsonError, symbolMatch } from "../utils.js";
|
|
20
|
+
// ── Mocks ──
|
|
21
|
+
vi.mock("../execution-log.js", () => ({ logExecution: vi.fn() }));
|
|
22
|
+
vi.mock("../client-id-tracker.js", () => ({
|
|
23
|
+
generateClientId: vi.fn(() => "test-id"),
|
|
24
|
+
logClientId: vi.fn(),
|
|
25
|
+
isOrderDuplicate: vi.fn(() => false),
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("../trade-validator.js", () => ({
|
|
28
|
+
validateTrade: vi.fn().mockResolvedValue({ valid: true, checks: [], warnings: [] }),
|
|
29
|
+
}));
|
|
30
|
+
function mockAdapter(overrides) {
|
|
31
|
+
return {
|
|
32
|
+
name: "test-exchange",
|
|
33
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "m1" }),
|
|
34
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "l1" }),
|
|
35
|
+
stopOrder: vi.fn().mockResolvedValue({ orderId: "s1" }),
|
|
36
|
+
cancelOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
37
|
+
cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: 0 }),
|
|
38
|
+
editOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
39
|
+
setLeverage: vi.fn().mockResolvedValue({ leverage: 10 }),
|
|
40
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
41
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
42
|
+
getBalance: vi.fn().mockResolvedValue({
|
|
43
|
+
equity: "10000", available: "8000", marginUsed: "2000", unrealizedPnl: "0",
|
|
44
|
+
}),
|
|
45
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
46
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
47
|
+
bids: [["100", "1"]], asks: [["101", "1"]],
|
|
48
|
+
}),
|
|
49
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
50
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
51
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
52
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
53
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
54
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createTradeProgram(adapter, json = false) {
|
|
59
|
+
const program = new Command();
|
|
60
|
+
program.exitOverride();
|
|
61
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
62
|
+
registerTradeCommands(program, async () => adapter, () => json);
|
|
63
|
+
return program;
|
|
64
|
+
}
|
|
65
|
+
function createMarketProgram(adapter, json = false) {
|
|
66
|
+
const program = new Command();
|
|
67
|
+
program.exitOverride();
|
|
68
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
69
|
+
registerMarketCommands(program, async () => adapter, () => json);
|
|
70
|
+
return program;
|
|
71
|
+
}
|
|
72
|
+
function createAccountProgram(adapter, json = false) {
|
|
73
|
+
const program = new Command();
|
|
74
|
+
program.exitOverride();
|
|
75
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
76
|
+
registerAccountCommands(program, async () => adapter, () => json);
|
|
77
|
+
return program;
|
|
78
|
+
}
|
|
79
|
+
async function run(program, args) {
|
|
80
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
81
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
82
|
+
try {
|
|
83
|
+
await program.parseAsync(["node", "perp", ...args]);
|
|
84
|
+
return { logCalls: log.mock.calls, errCalls: err.mock.calls };
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
log.mockRestore();
|
|
88
|
+
err.mockRestore();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getJsonOutput(calls) {
|
|
92
|
+
for (const call of calls) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(String(call[0]));
|
|
95
|
+
}
|
|
96
|
+
catch { /* not JSON */ }
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
beforeEach(() => vi.clearAllMocks());
|
|
101
|
+
// ══════════════════════════════════════════════════════════════
|
|
102
|
+
// 1. COMMAND INJECTION VIA ARGUMENTS
|
|
103
|
+
// ══════════════════════════════════════════════════════════════
|
|
104
|
+
describe("Command injection attempts", () => {
|
|
105
|
+
const injections = [
|
|
106
|
+
'; rm -rf /',
|
|
107
|
+
'$(whoami)',
|
|
108
|
+
'`whoami`',
|
|
109
|
+
'| cat /etc/passwd',
|
|
110
|
+
'&& curl evil.com',
|
|
111
|
+
'\n rm -rf /',
|
|
112
|
+
'"; DROP TABLE orders; --',
|
|
113
|
+
'<script>alert(1)</script>',
|
|
114
|
+
'{{7*7}}', // template injection
|
|
115
|
+
'${process.env.SECRET}', // env var interpolation
|
|
116
|
+
'__proto__',
|
|
117
|
+
'constructor',
|
|
118
|
+
];
|
|
119
|
+
it("symbol field: injection strings are passed as-is to adapter (not executed)", async () => {
|
|
120
|
+
for (const payload of injections) {
|
|
121
|
+
const adapter = mockAdapter();
|
|
122
|
+
const program = createTradeProgram(adapter);
|
|
123
|
+
try {
|
|
124
|
+
await run(program, ["trade", "market", payload, "buy", "0.1"]);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Commander may reject some chars — that's fine
|
|
128
|
+
}
|
|
129
|
+
// If adapter was called, verify the payload was passed as a string, not executed
|
|
130
|
+
if (adapter.marketOrder.mock.calls.length > 0) {
|
|
131
|
+
const calledSymbol = adapter.marketOrder.mock.calls[0][0];
|
|
132
|
+
expect(typeof calledSymbol).toBe("string");
|
|
133
|
+
// The important thing: it's uppercased string, not a shell command result
|
|
134
|
+
expect(calledSymbol).toBe(payload.toUpperCase());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
it("client-id: injection strings don't break JSON output", async () => {
|
|
139
|
+
const adapter = mockAdapter();
|
|
140
|
+
const program = createTradeProgram(adapter, true);
|
|
141
|
+
const { logCalls } = await run(program, [
|
|
142
|
+
"trade", "market", "BTC", "buy", "0.1",
|
|
143
|
+
"--client-id", '"; DROP TABLE orders; --',
|
|
144
|
+
]);
|
|
145
|
+
const output = getJsonOutput(logCalls);
|
|
146
|
+
// Must be valid JSON — no injection broke the envelope
|
|
147
|
+
expect(output).toBeDefined();
|
|
148
|
+
expect(output.ok).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// ══════════════════════════════════════════════════════════════
|
|
152
|
+
// 2. EXTREME / MALFORMED VALUES
|
|
153
|
+
// ══════════════════════════════════════════════════════════════
|
|
154
|
+
describe("Extreme and malformed numeric values", () => {
|
|
155
|
+
it("size = 0: passed to adapter as-is (adapter decides validity)", async () => {
|
|
156
|
+
const adapter = mockAdapter();
|
|
157
|
+
const program = createTradeProgram(adapter);
|
|
158
|
+
await run(program, ["trade", "market", "BTC", "buy", "0"]);
|
|
159
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0");
|
|
160
|
+
});
|
|
161
|
+
it("size = negative: Commander interprets '-1' as unknown option flag (known edge case)", async () => {
|
|
162
|
+
const adapter = mockAdapter();
|
|
163
|
+
const program = createTradeProgram(adapter);
|
|
164
|
+
// Commander parses "-1" as a flag, not an argument — this is expected behavior
|
|
165
|
+
// Agents/users must NOT pass negative sizes
|
|
166
|
+
try {
|
|
167
|
+
await run(program, ["trade", "market", "BTC", "buy", "-1"]);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
expect(e.message).toContain("unknown option");
|
|
171
|
+
}
|
|
172
|
+
// Adapter should NOT be called
|
|
173
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
it("size = NaN string: passed to adapter as-is", async () => {
|
|
176
|
+
const adapter = mockAdapter();
|
|
177
|
+
const program = createTradeProgram(adapter);
|
|
178
|
+
await run(program, ["trade", "market", "BTC", "buy", "NaN"]);
|
|
179
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "NaN");
|
|
180
|
+
});
|
|
181
|
+
it("size = Infinity: passed to adapter as-is", async () => {
|
|
182
|
+
const adapter = mockAdapter();
|
|
183
|
+
const program = createTradeProgram(adapter);
|
|
184
|
+
await run(program, ["trade", "market", "BTC", "buy", "Infinity"]);
|
|
185
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "Infinity");
|
|
186
|
+
});
|
|
187
|
+
it("size = absurdly large number: no crash", async () => {
|
|
188
|
+
const adapter = mockAdapter();
|
|
189
|
+
const program = createTradeProgram(adapter);
|
|
190
|
+
await run(program, ["trade", "market", "BTC", "buy", "999999999999999999999"]);
|
|
191
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "999999999999999999999");
|
|
192
|
+
});
|
|
193
|
+
it("size = tiny decimal: no precision loss as string", async () => {
|
|
194
|
+
const adapter = mockAdapter();
|
|
195
|
+
const program = createTradeProgram(adapter);
|
|
196
|
+
await run(program, ["trade", "market", "BTC", "buy", "0.000000001"]);
|
|
197
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.000000001");
|
|
198
|
+
});
|
|
199
|
+
it("price = 0 in limit order: passed through", async () => {
|
|
200
|
+
const adapter = mockAdapter();
|
|
201
|
+
const program = createTradeProgram(adapter);
|
|
202
|
+
await run(program, ["trade", "limit", "BTC", "buy", "0", "1"]);
|
|
203
|
+
expect(adapter.limitOrder).toHaveBeenCalledWith("BTC", "buy", "0", "1");
|
|
204
|
+
});
|
|
205
|
+
it("leverage = 0: doesn't crash", async () => {
|
|
206
|
+
const adapter = mockAdapter();
|
|
207
|
+
const program = createTradeProgram(adapter);
|
|
208
|
+
await run(program, ["trade", "leverage", "BTC", "0"]);
|
|
209
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 0, "cross");
|
|
210
|
+
});
|
|
211
|
+
it("leverage = 99999: passed through", async () => {
|
|
212
|
+
const adapter = mockAdapter();
|
|
213
|
+
const program = createTradeProgram(adapter);
|
|
214
|
+
await run(program, ["trade", "leverage", "BTC", "99999"]);
|
|
215
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 99999, "cross");
|
|
216
|
+
});
|
|
217
|
+
it("reduce percent = 0: errorAndExit", async () => {
|
|
218
|
+
const adapter = mockAdapter({
|
|
219
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
220
|
+
{ symbol: "BTC", side: "long", size: "1", entryPrice: "100000", markPrice: "100000", liquidationPrice: "90000", unrealizedPnl: "0", leverage: 10 },
|
|
221
|
+
]),
|
|
222
|
+
});
|
|
223
|
+
const program = createTradeProgram(adapter, true);
|
|
224
|
+
// percent <= 0 should error
|
|
225
|
+
try {
|
|
226
|
+
await run(program, ["trade", "reduce", "BTC", "0"]);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// errorAndExit calls process.exit which throws in test
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
it("reduce percent = 101: errorAndExit", async () => {
|
|
233
|
+
const adapter = mockAdapter();
|
|
234
|
+
const program = createTradeProgram(adapter, true);
|
|
235
|
+
try {
|
|
236
|
+
await run(program, ["trade", "reduce", "BTC", "101"]);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// errorAndExit calls process.exit
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// ══════════════════════════════════════════════════════════════
|
|
244
|
+
// 3. SPECIAL CHARACTERS & UNICODE IN SYMBOLS
|
|
245
|
+
// ══════════════════════════════════════════════════════════════
|
|
246
|
+
describe("Special characters and unicode in symbols", () => {
|
|
247
|
+
const weirdSymbols = [
|
|
248
|
+
"", // empty
|
|
249
|
+
" ", // space
|
|
250
|
+
"../../etc/passwd",
|
|
251
|
+
"BTC\x00ETH", // null byte
|
|
252
|
+
"BTC\nETH", // newline
|
|
253
|
+
"BTC\tETH", // tab
|
|
254
|
+
"🚀MOON", // emoji
|
|
255
|
+
"A".repeat(10000), // very long
|
|
256
|
+
"<img src=x onerror=alert(1)>", // XSS
|
|
257
|
+
"BTC&symbol=ETH", // query param injection
|
|
258
|
+
"%00%0d%0a", // URL encoding
|
|
259
|
+
];
|
|
260
|
+
it("weird symbols don't crash market mid", async () => {
|
|
261
|
+
for (const sym of weirdSymbols) {
|
|
262
|
+
if (!sym.trim())
|
|
263
|
+
continue; // Commander rejects truly empty args
|
|
264
|
+
const adapter = mockAdapter();
|
|
265
|
+
const program = createMarketProgram(adapter, true);
|
|
266
|
+
try {
|
|
267
|
+
await run(program, ["market", "mid", sym]);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Commander may reject — that's OK
|
|
271
|
+
}
|
|
272
|
+
// Key: no unhandled exceptions, no process crash
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
it("symbolMatch is safe with regex-special characters", () => {
|
|
276
|
+
// These should not throw even though they contain regex specials
|
|
277
|
+
const regexSpecials = ["BTC+PERP", "ETH.*", "SOL[0]", "DOT(1)", "BTC|ETH", "ATOM\\d+"];
|
|
278
|
+
for (const sym of regexSpecials) {
|
|
279
|
+
expect(() => symbolMatch(sym, "BTC")).not.toThrow();
|
|
280
|
+
expect(() => symbolMatch("BTC", sym)).not.toThrow();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
// ══════════════════════════════════════════════════════════════
|
|
285
|
+
// 4. PRIVATE KEY LEAKAGE IN ERROR MESSAGES
|
|
286
|
+
// ══════════════════════════════════════════════════════════════
|
|
287
|
+
describe("Private key leakage prevention", () => {
|
|
288
|
+
it("classifyError does not include stack traces with key material", () => {
|
|
289
|
+
// Simulate an error that might contain a key
|
|
290
|
+
const fakeKey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
|
291
|
+
const err = new Error(`Failed to sign with key ${fakeKey}`);
|
|
292
|
+
const classified = classifyError(err);
|
|
293
|
+
// The message field will contain the raw error — this is a known issue to flag
|
|
294
|
+
// But the code should be SIGNATURE_FAILED, not UNKNOWN
|
|
295
|
+
expect(classified.code).toBe("SIGNATURE_FAILED");
|
|
296
|
+
});
|
|
297
|
+
it("jsonError output does not expose internal stack traces", () => {
|
|
298
|
+
const err = jsonError("FATAL", "Something went wrong");
|
|
299
|
+
const json = JSON.stringify(err);
|
|
300
|
+
// Should not contain file paths or stack traces
|
|
301
|
+
expect(json).not.toContain("node_modules");
|
|
302
|
+
expect(json).not.toContain("at Object.");
|
|
303
|
+
expect(json).not.toContain(".ts:");
|
|
304
|
+
});
|
|
305
|
+
it("jsonOk output only contains data, not env vars", () => {
|
|
306
|
+
const data = { balance: "100", secret: process.env.HOME };
|
|
307
|
+
const result = jsonOk(data);
|
|
308
|
+
const json = JSON.stringify(result);
|
|
309
|
+
// The data field contains what we put in — that's expected
|
|
310
|
+
// But jsonOk itself should not inject env vars
|
|
311
|
+
expect(result.ok).toBe(true);
|
|
312
|
+
expect(result.meta?.timestamp).toBeDefined();
|
|
313
|
+
// meta should only have timestamp, not env vars
|
|
314
|
+
const metaKeys = Object.keys(result.meta ?? {});
|
|
315
|
+
expect(metaKeys).not.toContain("env");
|
|
316
|
+
expect(metaKeys).not.toContain("process");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
// ══════════════════════════════════════════════════════════════
|
|
320
|
+
// 5. JSON ENVELOPE INTEGRITY ATTACKS
|
|
321
|
+
// ══════════════════════════════════════════════════════════════
|
|
322
|
+
describe("JSON envelope integrity", () => {
|
|
323
|
+
it("jsonOk with circular reference throws on stringify, not silently corrupts", () => {
|
|
324
|
+
const obj = { a: 1 };
|
|
325
|
+
obj.self = obj; // circular
|
|
326
|
+
const result = jsonOk(obj);
|
|
327
|
+
expect(() => JSON.stringify(result)).toThrow(); // TypeError: circular
|
|
328
|
+
});
|
|
329
|
+
it("jsonOk with undefined values: JSON.stringify drops them cleanly", () => {
|
|
330
|
+
const result = jsonOk({ value: undefined, name: "test" });
|
|
331
|
+
const json = JSON.stringify(result);
|
|
332
|
+
const parsed = JSON.parse(json);
|
|
333
|
+
expect(parsed.data.name).toBe("test");
|
|
334
|
+
// undefined is dropped by JSON.stringify
|
|
335
|
+
expect("value" in parsed.data).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
it("jsonError with very long message doesn't crash", () => {
|
|
338
|
+
const longMsg = "A".repeat(100000);
|
|
339
|
+
const result = jsonError("FATAL", longMsg);
|
|
340
|
+
expect(result.ok).toBe(false);
|
|
341
|
+
expect(result.error?.message.length).toBe(100000);
|
|
342
|
+
// Should still be valid JSON
|
|
343
|
+
const json = JSON.stringify(result);
|
|
344
|
+
const parsed = JSON.parse(json);
|
|
345
|
+
expect(parsed.ok).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
it("jsonOk with __proto__ key doesn't cause prototype pollution", () => {
|
|
348
|
+
const evil = JSON.parse('{"__proto__": {"polluted": true}}');
|
|
349
|
+
const result = jsonOk(evil);
|
|
350
|
+
const json = JSON.stringify(result);
|
|
351
|
+
const parsed = JSON.parse(json);
|
|
352
|
+
// The parsed object should NOT have polluted the prototype
|
|
353
|
+
expect({}.polluted).toBeUndefined();
|
|
354
|
+
expect(parsed.data.__proto__).toBeDefined(); // It's just a regular key
|
|
355
|
+
});
|
|
356
|
+
it("jsonOk with constructor key is safe", () => {
|
|
357
|
+
const result = jsonOk({ constructor: "evil", toString: "override" });
|
|
358
|
+
const json = JSON.stringify(result);
|
|
359
|
+
const parsed = JSON.parse(json);
|
|
360
|
+
expect(parsed.data.constructor).toBe("evil");
|
|
361
|
+
// Object constructor should not be overridden
|
|
362
|
+
expect(typeof ({}).constructor).toBe("function");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
// ══════════════════════════════════════════════════════════════
|
|
366
|
+
// 6. INVALID SIDE PARAMETER
|
|
367
|
+
// ══════════════════════════════════════════════════════════════
|
|
368
|
+
describe("Invalid side parameter", () => {
|
|
369
|
+
it("side = 'BUY' (uppercase) still works (lowercased internally)", async () => {
|
|
370
|
+
const adapter = mockAdapter();
|
|
371
|
+
const program = createTradeProgram(adapter);
|
|
372
|
+
await run(program, ["trade", "market", "BTC", "BUY", "0.1"]);
|
|
373
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.1");
|
|
374
|
+
});
|
|
375
|
+
it("side = 'SELL' (uppercase) still works", async () => {
|
|
376
|
+
const adapter = mockAdapter();
|
|
377
|
+
const program = createTradeProgram(adapter);
|
|
378
|
+
await run(program, ["trade", "market", "BTC", "SELL", "0.1"]);
|
|
379
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.1");
|
|
380
|
+
});
|
|
381
|
+
it("side = 'long' triggers errorAndExit", async () => {
|
|
382
|
+
const adapter = mockAdapter();
|
|
383
|
+
const program = createTradeProgram(adapter);
|
|
384
|
+
try {
|
|
385
|
+
await run(program, ["trade", "market", "BTC", "long", "0.1"]);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// errorAndExit calls process.exit
|
|
389
|
+
}
|
|
390
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
391
|
+
});
|
|
392
|
+
it("side = '' (empty) triggers errorAndExit", async () => {
|
|
393
|
+
const adapter = mockAdapter();
|
|
394
|
+
const program = createTradeProgram(adapter);
|
|
395
|
+
try {
|
|
396
|
+
await run(program, ["trade", "market", "BTC", "", "0.1"]);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Commander may handle this
|
|
400
|
+
}
|
|
401
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
// ══════════════════════════════════════════════════════════════
|
|
405
|
+
// 7. ERROR CLASSIFICATION ROBUSTNESS
|
|
406
|
+
// ══════════════════════════════════════════════════════════════
|
|
407
|
+
describe("Error classification edge cases", () => {
|
|
408
|
+
it("classifyError handles null", () => {
|
|
409
|
+
const result = classifyError(null);
|
|
410
|
+
expect(result.code).toBeDefined();
|
|
411
|
+
expect(typeof result.message).toBe("string");
|
|
412
|
+
});
|
|
413
|
+
it("classifyError handles undefined", () => {
|
|
414
|
+
const result = classifyError(undefined);
|
|
415
|
+
expect(result.code).toBeDefined();
|
|
416
|
+
});
|
|
417
|
+
it("classifyError handles number", () => {
|
|
418
|
+
const result = classifyError(42);
|
|
419
|
+
expect(result.message).toBe("42");
|
|
420
|
+
});
|
|
421
|
+
it("classifyError handles object without message", () => {
|
|
422
|
+
const result = classifyError({ status: 500 });
|
|
423
|
+
expect(result.code).toBeDefined();
|
|
424
|
+
});
|
|
425
|
+
it("classifyError handles empty string", () => {
|
|
426
|
+
const result = classifyError(new Error(""));
|
|
427
|
+
expect(result.code).toBe("UNKNOWN");
|
|
428
|
+
});
|
|
429
|
+
it("classifyError handles very long error message", () => {
|
|
430
|
+
const longMsg = "x".repeat(100000);
|
|
431
|
+
const result = classifyError(new Error(longMsg));
|
|
432
|
+
expect(result.message.length).toBe(100000);
|
|
433
|
+
});
|
|
434
|
+
it("classifyError handles error with nested cause", () => {
|
|
435
|
+
const inner = new Error("inner insufficient balance");
|
|
436
|
+
const outer = new Error("Outer error", { cause: inner });
|
|
437
|
+
// classifyError uses outer.message, not cause
|
|
438
|
+
const result = classifyError(outer);
|
|
439
|
+
expect(result.code).toBe("UNKNOWN"); // "Outer error" doesn't match patterns
|
|
440
|
+
});
|
|
441
|
+
it("PerpError preserves structured info through serialize/deserialize", () => {
|
|
442
|
+
const err = new PerpError("RATE_LIMITED", "Too many requests", { waitMs: 1000 });
|
|
443
|
+
expect(err.structured.code).toBe("RATE_LIMITED");
|
|
444
|
+
expect(err.structured.retryable).toBe(true);
|
|
445
|
+
expect(err.structured.details?.waitMs).toBe(1000);
|
|
446
|
+
// Simulate JSON round-trip (agent receiving error)
|
|
447
|
+
const json = JSON.stringify(jsonError(err.structured.code, err.message));
|
|
448
|
+
const parsed = JSON.parse(json);
|
|
449
|
+
expect(parsed.error.code).toBe("RATE_LIMITED");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
// ══════════════════════════════════════════════════════════════
|
|
453
|
+
// 8. CONCURRENT / RAPID-FIRE SAFETY
|
|
454
|
+
// ══════════════════════════════════════════════════════════════
|
|
455
|
+
describe("Concurrent operations safety", () => {
|
|
456
|
+
it("multiple sequential market mid calls each return correct symbol", async () => {
|
|
457
|
+
// Run sequentially to avoid console.log spy interference
|
|
458
|
+
const symbols = [];
|
|
459
|
+
for (const sym of ["BTC", "ETH", "SOL"]) {
|
|
460
|
+
const adapter = mockAdapter();
|
|
461
|
+
const program = createMarketProgram(adapter, true);
|
|
462
|
+
const { logCalls } = await run(program, ["market", "mid", sym]);
|
|
463
|
+
const out = getJsonOutput(logCalls);
|
|
464
|
+
symbols.push(out?.data?.symbol);
|
|
465
|
+
}
|
|
466
|
+
expect(symbols).toContain("BTC");
|
|
467
|
+
expect(symbols).toContain("ETH");
|
|
468
|
+
expect(symbols).toContain("SOL");
|
|
469
|
+
});
|
|
470
|
+
it("adapter errors in one call don't affect another", async () => {
|
|
471
|
+
const adapter = mockAdapter({
|
|
472
|
+
getOrderbook: vi.fn()
|
|
473
|
+
.mockRejectedValueOnce(new Error("Timeout"))
|
|
474
|
+
.mockResolvedValueOnce({ bids: [["100", "1"]], asks: [["101", "1"]] }),
|
|
475
|
+
});
|
|
476
|
+
// First call fails
|
|
477
|
+
const prog1 = createMarketProgram(adapter, true);
|
|
478
|
+
const { logCalls: calls1 } = await run(prog1, ["market", "mid", "BTC"]);
|
|
479
|
+
const out1 = getJsonOutput(calls1);
|
|
480
|
+
expect(out1?.ok).toBe(false);
|
|
481
|
+
// Second call succeeds
|
|
482
|
+
const prog2 = createMarketProgram(adapter, true);
|
|
483
|
+
const { logCalls: calls2 } = await run(prog2, ["market", "mid", "ETH"]);
|
|
484
|
+
const out2 = getJsonOutput(calls2);
|
|
485
|
+
expect(out2?.ok).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
// ══════════════════════════════════════════════════════════════
|
|
489
|
+
// 9. PATH TRAVERSAL IN FILE ARGUMENTS
|
|
490
|
+
// ══════════════════════════════════════════════════════════════
|
|
491
|
+
describe("Path traversal attempts", () => {
|
|
492
|
+
it("symbolMatch doesn't interpret path separators specially", () => {
|
|
493
|
+
expect(symbolMatch("../../../etc/passwd", "BTC")).toBe(false);
|
|
494
|
+
expect(symbolMatch("BTC", "../../../etc/passwd")).toBe(false);
|
|
495
|
+
});
|
|
496
|
+
it("orderId with path traversal is just a string", async () => {
|
|
497
|
+
const adapter = mockAdapter({
|
|
498
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
499
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
500
|
+
});
|
|
501
|
+
const program = createTradeProgram(adapter, true);
|
|
502
|
+
const { logCalls } = await run(program, ["trade", "status", "../../../etc/passwd"]);
|
|
503
|
+
const output = getJsonOutput(logCalls);
|
|
504
|
+
// Should just return ORDER_NOT_FOUND, not attempt file read
|
|
505
|
+
expect(output?.ok).toBe(false);
|
|
506
|
+
expect(output?.error?.code).toBe("ORDER_NOT_FOUND");
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
// ══════════════════════════════════════════════════════════════
|
|
510
|
+
// 10. ACCOUNT MARGIN WITH MALFORMED POSITION DATA
|
|
511
|
+
// ══════════════════════════════════════════════════════════════
|
|
512
|
+
describe("Account margin with malformed data", () => {
|
|
513
|
+
it("handles NaN markPrice without crash", async () => {
|
|
514
|
+
const adapter = mockAdapter({
|
|
515
|
+
getPositions: vi.fn().mockResolvedValue([{
|
|
516
|
+
symbol: "BTC", side: "long", size: "1",
|
|
517
|
+
entryPrice: "NaN", markPrice: "NaN",
|
|
518
|
+
liquidationPrice: "N/A", unrealizedPnl: "0", leverage: 10,
|
|
519
|
+
}]),
|
|
520
|
+
});
|
|
521
|
+
const program = createAccountProgram(adapter, true);
|
|
522
|
+
const { logCalls } = await run(program, ["account", "margin", "BTC"]);
|
|
523
|
+
const output = getJsonOutput(logCalls);
|
|
524
|
+
// Should not crash — NaN math produces NaN which becomes "NaN" string
|
|
525
|
+
expect(output).toBeDefined();
|
|
526
|
+
expect(output?.ok).toBe(true);
|
|
527
|
+
});
|
|
528
|
+
it("handles zero equity (division by zero in marginPct)", async () => {
|
|
529
|
+
const adapter = mockAdapter({
|
|
530
|
+
getBalance: vi.fn().mockResolvedValue({
|
|
531
|
+
equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0",
|
|
532
|
+
}),
|
|
533
|
+
getPositions: vi.fn().mockResolvedValue([{
|
|
534
|
+
symbol: "BTC", side: "long", size: "0.1",
|
|
535
|
+
entryPrice: "100000", markPrice: "100000",
|
|
536
|
+
liquidationPrice: "90000", unrealizedPnl: "0", leverage: 10,
|
|
537
|
+
}]),
|
|
538
|
+
});
|
|
539
|
+
const program = createAccountProgram(adapter, true);
|
|
540
|
+
const { logCalls } = await run(program, ["account", "margin", "BTC"]);
|
|
541
|
+
const output = getJsonOutput(logCalls);
|
|
542
|
+
expect(output.ok).toBe(true);
|
|
543
|
+
// equity=0 → marginPct should be 0, not Infinity
|
|
544
|
+
expect(output.data.marginPctOfEquity).toBe("0.00");
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
// ══════════════════════════════════════════════════════════════
|
|
548
|
+
// 11. TRADE FILLS WITH CRAFTED TRADE DATA
|
|
549
|
+
// ══════════════════════════════════════════════════════════════
|
|
550
|
+
describe("Trade fills with crafted data", () => {
|
|
551
|
+
it("handles trades with negative prices", async () => {
|
|
552
|
+
const adapter = mockAdapter({
|
|
553
|
+
getTradeHistory: vi.fn().mockResolvedValue([
|
|
554
|
+
{ time: Date.now(), symbol: "BTC", side: "buy", price: "-100", size: "1", fee: "-5" },
|
|
555
|
+
]),
|
|
556
|
+
});
|
|
557
|
+
const program = createTradeProgram(adapter, true);
|
|
558
|
+
const { logCalls } = await run(program, ["trade", "fills"]);
|
|
559
|
+
const output = getJsonOutput(logCalls);
|
|
560
|
+
expect(output.ok).toBe(true);
|
|
561
|
+
expect(output.data).toHaveLength(1);
|
|
562
|
+
});
|
|
563
|
+
it("handles trades with extremely large timestamps", async () => {
|
|
564
|
+
const adapter = mockAdapter({
|
|
565
|
+
getTradeHistory: vi.fn().mockResolvedValue([
|
|
566
|
+
{ time: 99999999999999, symbol: "BTC", side: "buy", price: "100", size: "1", fee: "0" },
|
|
567
|
+
]),
|
|
568
|
+
});
|
|
569
|
+
const program = createTradeProgram(adapter, true);
|
|
570
|
+
const { logCalls } = await run(program, ["trade", "fills"]);
|
|
571
|
+
const output = getJsonOutput(logCalls);
|
|
572
|
+
expect(output.ok).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|