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.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/__tests__/alert-logic.test.d.ts +1 -0
  4. package/dist/__tests__/alert-logic.test.js +107 -0
  5. package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
  6. package/dist/__tests__/arb-auto-3dex.test.js +397 -0
  7. package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
  8. package/dist/__tests__/arb-history-stats.test.js +176 -0
  9. package/dist/__tests__/arb-logic.test.d.ts +1 -0
  10. package/dist/__tests__/arb-logic.test.js +84 -0
  11. package/dist/__tests__/arb-manage.test.d.ts +1 -0
  12. package/dist/__tests__/arb-manage.test.js +253 -0
  13. package/dist/__tests__/arb-new-features.test.d.ts +1 -0
  14. package/dist/__tests__/arb-new-features.test.js +457 -0
  15. package/dist/__tests__/arb-sizing.test.d.ts +1 -0
  16. package/dist/__tests__/arb-sizing.test.js +48 -0
  17. package/dist/__tests__/arb-state.test.d.ts +1 -0
  18. package/dist/__tests__/arb-state.test.js +284 -0
  19. package/dist/__tests__/arb-userflow.test.d.ts +1 -0
  20. package/dist/__tests__/arb-userflow.test.js +945 -0
  21. package/dist/__tests__/arb-utils.test.d.ts +1 -0
  22. package/dist/__tests__/arb-utils.test.js +264 -0
  23. package/dist/__tests__/bot-conditions.test.d.ts +1 -0
  24. package/dist/__tests__/bot-conditions.test.js +341 -0
  25. package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
  26. package/dist/__tests__/client-id-tracker.test.js +137 -0
  27. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
  28. package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
  29. package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
  30. package/dist/__tests__/commands/order-intent.test.js +600 -0
  31. package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
  32. package/dist/__tests__/commands/trade-commands.test.js +821 -0
  33. package/dist/__tests__/config.test.d.ts +1 -0
  34. package/dist/__tests__/config.test.js +86 -0
  35. package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
  36. package/dist/__tests__/cross-chain-margin.test.js +287 -0
  37. package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
  38. package/dist/__tests__/dex-asset-map.test.js +191 -0
  39. package/dist/__tests__/errors.test.d.ts +1 -0
  40. package/dist/__tests__/errors.test.js +110 -0
  41. package/dist/__tests__/event-stream.test.d.ts +1 -0
  42. package/dist/__tests__/event-stream.test.js +276 -0
  43. package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
  44. package/dist/__tests__/exchanges/interface.test.js +132 -0
  45. package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
  46. package/dist/__tests__/exchanges/mock-adapter.js +137 -0
  47. package/dist/__tests__/execution-log.test.d.ts +1 -0
  48. package/dist/__tests__/execution-log.test.js +106 -0
  49. package/dist/__tests__/funding-calc.test.d.ts +1 -0
  50. package/dist/__tests__/funding-calc.test.js +71 -0
  51. package/dist/__tests__/funding-history.test.d.ts +1 -0
  52. package/dist/__tests__/funding-history.test.js +343 -0
  53. package/dist/__tests__/funding-rates.test.d.ts +1 -0
  54. package/dist/__tests__/funding-rates.test.js +342 -0
  55. package/dist/__tests__/funding.test.d.ts +1 -0
  56. package/dist/__tests__/funding.test.js +173 -0
  57. package/dist/__tests__/gap-logic.test.d.ts +1 -0
  58. package/dist/__tests__/gap-logic.test.js +43 -0
  59. package/dist/__tests__/hip3-dex.test.d.ts +1 -0
  60. package/dist/__tests__/hip3-dex.test.js +234 -0
  61. package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
  62. package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
  63. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
  64. package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
  65. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
  66. package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
  67. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
  68. package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
  69. package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
  70. package/dist/__tests__/integration/bridge.integration.test.js +309 -0
  71. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
  72. package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
  73. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
  74. package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
  75. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
  76. package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
  77. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
  78. package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
  79. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
  80. package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
  81. package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
  82. package/dist/__tests__/integration/lighter.integration.test.js +53 -0
  83. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
  84. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
  85. package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
  86. package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
  87. package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
  88. package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
  89. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
  90. package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
  91. package/dist/__tests__/liquidity.test.d.ts +1 -0
  92. package/dist/__tests__/liquidity.test.js +225 -0
  93. package/dist/__tests__/plan-executor.test.d.ts +1 -0
  94. package/dist/__tests__/plan-executor.test.js +314 -0
  95. package/dist/__tests__/position-history.test.d.ts +1 -0
  96. package/dist/__tests__/position-history.test.js +367 -0
  97. package/dist/__tests__/retry.test.d.ts +1 -0
  98. package/dist/__tests__/retry.test.js +310 -0
  99. package/dist/__tests__/risk-assessment.test.d.ts +1 -0
  100. package/dist/__tests__/risk-assessment.test.js +145 -0
  101. package/dist/__tests__/security-adversarial.test.d.ts +1 -0
  102. package/dist/__tests__/security-adversarial.test.js +574 -0
  103. package/dist/__tests__/strategies.test.d.ts +1 -0
  104. package/dist/__tests__/strategies.test.js +539 -0
  105. package/dist/__tests__/trade-execution.test.d.ts +1 -0
  106. package/dist/__tests__/trade-execution.test.js +129 -0
  107. package/dist/__tests__/trade-validator.test.d.ts +1 -0
  108. package/dist/__tests__/trade-validator.test.js +655 -0
  109. package/dist/__tests__/utils.test.d.ts +1 -0
  110. package/dist/__tests__/utils.test.js +76 -0
  111. package/dist/api/public/hyperliquid.d.ts +18 -0
  112. package/dist/api/public/hyperliquid.js +82 -0
  113. package/dist/api/public/index.d.ts +8 -0
  114. package/dist/api/public/index.js +8 -0
  115. package/dist/api/public/lighter.d.ts +24 -0
  116. package/dist/api/public/lighter.js +100 -0
  117. package/dist/api/public/pacifica.d.ts +17 -0
  118. package/dist/api/public/pacifica.js +54 -0
  119. package/dist/api/public/urls.d.ts +12 -0
  120. package/dist/api/public/urls.js +33 -0
  121. package/dist/arb/history-stats.d.ts +44 -0
  122. package/dist/arb/history-stats.js +135 -0
  123. package/dist/arb/index.d.ts +4 -0
  124. package/dist/arb/index.js +4 -0
  125. package/dist/arb/sizing.d.ts +23 -0
  126. package/dist/arb/sizing.js +96 -0
  127. package/dist/arb/state.d.ts +51 -0
  128. package/dist/arb/state.js +112 -0
  129. package/dist/arb/utils.d.ts +81 -0
  130. package/dist/arb/utils.js +267 -0
  131. package/dist/arb-history-stats.d.ts +5 -0
  132. package/dist/arb-history-stats.js +5 -0
  133. package/dist/arb-sizing.d.ts +5 -0
  134. package/dist/arb-sizing.js +5 -0
  135. package/dist/arb-state.d.ts +5 -0
  136. package/dist/arb-state.js +5 -0
  137. package/dist/arb-utils.d.ts +5 -0
  138. package/dist/arb-utils.js +5 -0
  139. package/dist/bot/conditions.d.ts +32 -0
  140. package/dist/bot/conditions.js +141 -0
  141. package/dist/bot/config.d.ts +76 -0
  142. package/dist/bot/config.js +160 -0
  143. package/dist/bot/engine.d.ts +8 -0
  144. package/dist/bot/engine.js +519 -0
  145. package/dist/bot/presets.d.ts +11 -0
  146. package/dist/bot/presets.js +296 -0
  147. package/dist/bridge-engine.d.ts +133 -0
  148. package/dist/bridge-engine.js +1487 -0
  149. package/dist/cache.d.ts +25 -0
  150. package/dist/cache.js +99 -0
  151. package/dist/cli-spec.d.ts +50 -0
  152. package/dist/cli-spec.js +75 -0
  153. package/dist/client-id-tracker.d.ts +25 -0
  154. package/dist/client-id-tracker.js +76 -0
  155. package/dist/commands/account.d.ts +3 -0
  156. package/dist/commands/account.js +425 -0
  157. package/dist/commands/agent.d.ts +3 -0
  158. package/dist/commands/agent.js +386 -0
  159. package/dist/commands/alert.d.ts +2 -0
  160. package/dist/commands/alert.js +421 -0
  161. package/dist/commands/analytics.d.ts +3 -0
  162. package/dist/commands/analytics.js +311 -0
  163. package/dist/commands/arb/index.d.ts +3 -0
  164. package/dist/commands/arb/index.js +921 -0
  165. package/dist/commands/arb-auto.d.ts +54 -0
  166. package/dist/commands/arb-auto.js +1328 -0
  167. package/dist/commands/arb-manage.d.ts +5 -0
  168. package/dist/commands/arb-manage.js +5 -0
  169. package/dist/commands/arb.d.ts +2 -0
  170. package/dist/commands/arb.js +347 -0
  171. package/dist/commands/backtest.d.ts +2 -0
  172. package/dist/commands/backtest.js +327 -0
  173. package/dist/commands/bot.d.ts +3 -0
  174. package/dist/commands/bot.js +412 -0
  175. package/dist/commands/bridge.d.ts +2 -0
  176. package/dist/commands/bridge.js +396 -0
  177. package/dist/commands/dashboard.d.ts +3 -0
  178. package/dist/commands/dashboard.js +176 -0
  179. package/dist/commands/deposit.d.ts +4 -0
  180. package/dist/commands/deposit.js +573 -0
  181. package/dist/commands/dex.d.ts +3 -0
  182. package/dist/commands/dex.js +114 -0
  183. package/dist/commands/env.d.ts +2 -0
  184. package/dist/commands/env.js +136 -0
  185. package/dist/commands/funding.d.ts +2 -0
  186. package/dist/commands/funding.js +347 -0
  187. package/dist/commands/gap.d.ts +2 -0
  188. package/dist/commands/gap.js +305 -0
  189. package/dist/commands/health.d.ts +2 -0
  190. package/dist/commands/health.js +67 -0
  191. package/dist/commands/history.d.ts +2 -0
  192. package/dist/commands/history.js +235 -0
  193. package/dist/commands/init.d.ts +15 -0
  194. package/dist/commands/init.js +266 -0
  195. package/dist/commands/jobs.d.ts +2 -0
  196. package/dist/commands/jobs.js +133 -0
  197. package/dist/commands/manage.d.ts +4 -0
  198. package/dist/commands/manage.js +309 -0
  199. package/dist/commands/market.d.ts +3 -0
  200. package/dist/commands/market.js +225 -0
  201. package/dist/commands/plan.d.ts +3 -0
  202. package/dist/commands/plan.js +95 -0
  203. package/dist/commands/portfolio.d.ts +3 -0
  204. package/dist/commands/portfolio.js +169 -0
  205. package/dist/commands/rebalance.d.ts +3 -0
  206. package/dist/commands/rebalance.js +293 -0
  207. package/dist/commands/risk.d.ts +3 -0
  208. package/dist/commands/risk.js +169 -0
  209. package/dist/commands/run.d.ts +3 -0
  210. package/dist/commands/run.js +202 -0
  211. package/dist/commands/settings.d.ts +2 -0
  212. package/dist/commands/settings.js +102 -0
  213. package/dist/commands/stream.d.ts +5 -0
  214. package/dist/commands/stream.js +123 -0
  215. package/dist/commands/trade.d.ts +3 -0
  216. package/dist/commands/trade.js +1273 -0
  217. package/dist/commands/wallet.d.ts +14 -0
  218. package/dist/commands/wallet.js +602 -0
  219. package/dist/commands/withdraw.d.ts +3 -0
  220. package/dist/commands/withdraw.js +187 -0
  221. package/dist/config.d.ts +5 -0
  222. package/dist/config.js +68 -0
  223. package/dist/cross-chain-margin.d.ts +46 -0
  224. package/dist/cross-chain-margin.js +107 -0
  225. package/dist/dashboard/server.d.ts +80 -0
  226. package/dist/dashboard/server.js +340 -0
  227. package/dist/dashboard/ui.d.ts +4 -0
  228. package/dist/dashboard/ui.js +538 -0
  229. package/dist/dashboard/ws-feeds.d.ts +29 -0
  230. package/dist/dashboard/ws-feeds.js +660 -0
  231. package/dist/dex-asset-map.d.ts +80 -0
  232. package/dist/dex-asset-map.js +201 -0
  233. package/dist/errors.d.ts +109 -0
  234. package/dist/errors.js +84 -0
  235. package/dist/event-stream.d.ts +25 -0
  236. package/dist/event-stream.js +168 -0
  237. package/dist/exchanges/hyperliquid.d.ts +212 -0
  238. package/dist/exchanges/hyperliquid.js +931 -0
  239. package/dist/exchanges/interface.d.ts +95 -0
  240. package/dist/exchanges/interface.js +5 -0
  241. package/dist/exchanges/lighter.d.ts +159 -0
  242. package/dist/exchanges/lighter.js +793 -0
  243. package/dist/exchanges/pacifica.d.ts +51 -0
  244. package/dist/exchanges/pacifica.js +248 -0
  245. package/dist/execution-log.d.ts +36 -0
  246. package/dist/execution-log.js +102 -0
  247. package/dist/funding/history.d.ts +63 -0
  248. package/dist/funding/history.js +266 -0
  249. package/dist/funding/index.d.ts +3 -0
  250. package/dist/funding/index.js +3 -0
  251. package/dist/funding/normalize.d.ts +39 -0
  252. package/dist/funding/normalize.js +66 -0
  253. package/dist/funding/rates.d.ts +45 -0
  254. package/dist/funding/rates.js +172 -0
  255. package/dist/funding-history.d.ts +5 -0
  256. package/dist/funding-history.js +5 -0
  257. package/dist/funding-rates.d.ts +5 -0
  258. package/dist/funding-rates.js +5 -0
  259. package/dist/funding.d.ts +5 -0
  260. package/dist/funding.js +5 -0
  261. package/dist/index.d.ts +2 -0
  262. package/dist/index.js +458 -0
  263. package/dist/jobs.d.ts +37 -0
  264. package/dist/jobs.js +152 -0
  265. package/dist/liquidity.d.ts +34 -0
  266. package/dist/liquidity.js +100 -0
  267. package/dist/mcp-server.d.ts +9 -0
  268. package/dist/mcp-server.js +1206 -0
  269. package/dist/pacifica/client.d.ts +111 -0
  270. package/dist/pacifica/client.js +310 -0
  271. package/dist/pacifica/constants.d.ts +27 -0
  272. package/dist/pacifica/constants.js +47 -0
  273. package/dist/pacifica/deposit.d.ts +14 -0
  274. package/dist/pacifica/deposit.js +78 -0
  275. package/dist/pacifica/index.d.ts +6 -0
  276. package/dist/pacifica/index.js +11 -0
  277. package/dist/pacifica/signing.d.ts +49 -0
  278. package/dist/pacifica/signing.js +97 -0
  279. package/dist/pacifica/types/account.d.ts +42 -0
  280. package/dist/pacifica/types/account.js +1 -0
  281. package/dist/pacifica/types/index.d.ts +6 -0
  282. package/dist/pacifica/types/index.js +6 -0
  283. package/dist/pacifica/types/lake.d.ts +18 -0
  284. package/dist/pacifica/types/lake.js +1 -0
  285. package/dist/pacifica/types/market.d.ts +64 -0
  286. package/dist/pacifica/types/market.js +1 -0
  287. package/dist/pacifica/types/order.d.ts +92 -0
  288. package/dist/pacifica/types/order.js +1 -0
  289. package/dist/pacifica/types/position.d.ts +25 -0
  290. package/dist/pacifica/types/position.js +1 -0
  291. package/dist/pacifica/types/ws.d.ts +34 -0
  292. package/dist/pacifica/types/ws.js +41 -0
  293. package/dist/pacifica/ws-client.d.ts +42 -0
  294. package/dist/pacifica/ws-client.js +180 -0
  295. package/dist/plan-executor.d.ts +48 -0
  296. package/dist/plan-executor.js +280 -0
  297. package/dist/position-history.d.ts +68 -0
  298. package/dist/position-history.js +222 -0
  299. package/dist/rebalance.d.ts +64 -0
  300. package/dist/rebalance.js +142 -0
  301. package/dist/retry.d.ts +74 -0
  302. package/dist/retry.js +129 -0
  303. package/dist/risk.d.ts +48 -0
  304. package/dist/risk.js +156 -0
  305. package/dist/settings.d.ts +19 -0
  306. package/dist/settings.js +45 -0
  307. package/dist/shared-api.d.ts +5 -0
  308. package/dist/shared-api.js +5 -0
  309. package/dist/strategies/dca.d.ts +25 -0
  310. package/dist/strategies/dca.js +114 -0
  311. package/dist/strategies/funding-arb.d.ts +15 -0
  312. package/dist/strategies/funding-arb.js +281 -0
  313. package/dist/strategies/grid.d.ts +34 -0
  314. package/dist/strategies/grid.js +185 -0
  315. package/dist/strategies/trailing-stop.d.ts +17 -0
  316. package/dist/strategies/trailing-stop.js +121 -0
  317. package/dist/strategies/twap.d.ts +20 -0
  318. package/dist/strategies/twap.js +78 -0
  319. package/dist/trade-validator.d.ts +39 -0
  320. package/dist/trade-validator.js +154 -0
  321. package/dist/utils.d.ts +38 -0
  322. package/dist/utils.js +110 -0
  323. package/package.json +63 -0
  324. package/skills/perp-cli/SKILL.md +149 -0
  325. 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 {};