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,309 @@
1
+ /**
2
+ * Integration tests for bridge engine — READ-ONLY operations only.
3
+ *
4
+ * Tests real API calls to:
5
+ * - deBridge DLN quote API (GET, no signing)
6
+ * - Circle CCTP fee API (GET)
7
+ * - deBridge status API (GET)
8
+ * - getCctpQuote() (pure calculation, no RPC)
9
+ * - getBestQuote() (CCTP preferred, deBridge fallback)
10
+ *
11
+ * NO transactions are executed. NO funds are spent.
12
+ *
13
+ * Note: deBridge API has strict rate limits (~5 req/min).
14
+ * Tests are structured to minimize API calls and run sequentially.
15
+ */
16
+ import { describe, it, expect } from "vitest";
17
+ import { getDebridgeQuote, getCctpQuote, getBestQuote, checkDebridgeStatus, CHAIN_IDS, USDC_ADDRESSES, EXCHANGE_TO_CHAIN, } from "../../bridge-engine.js";
18
+ // Dummy addresses for quote-only calls (never used for signing)
19
+ const DUMMY_EVM = "0x0000000000000000000000000000000000000001";
20
+ const DUMMY_SOLANA = "11111111111111111111111111111111";
21
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
22
+ describe("Bridge Integration — Read-Only", { timeout: 60000 }, () => {
23
+ // ══════════════════════════════════════════════════════════
24
+ // Constants & Configuration (no API calls)
25
+ // ══════════════════════════════════════════════════════════
26
+ describe("chain constants", () => {
27
+ it("all chains have valid chain IDs", () => {
28
+ for (const [chain, id] of Object.entries(CHAIN_IDS)) {
29
+ expect(typeof id).toBe("number");
30
+ expect(id).toBeGreaterThan(0);
31
+ expect(chain.length).toBeGreaterThan(0);
32
+ }
33
+ });
34
+ it("all USDC addresses are valid format", () => {
35
+ for (const [chain, addr] of Object.entries(USDC_ADDRESSES)) {
36
+ if (chain === "solana") {
37
+ expect(addr.length).toBeGreaterThan(30);
38
+ expect(addr.length).toBeLessThan(50);
39
+ }
40
+ else {
41
+ expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/);
42
+ }
43
+ }
44
+ });
45
+ it("exchange-to-chain mapping covers all exchanges", () => {
46
+ expect(EXCHANGE_TO_CHAIN.pacifica).toBe("solana");
47
+ expect(EXCHANGE_TO_CHAIN.hyperliquid).toBe("hyperliquid");
48
+ expect(EXCHANGE_TO_CHAIN.lighter).toBe("arbitrum");
49
+ });
50
+ it("CCTP-supported chains have matching USDC addresses", () => {
51
+ for (const chain of ["solana", "arbitrum", "base"]) {
52
+ expect(USDC_ADDRESSES[chain]).toBeDefined();
53
+ }
54
+ });
55
+ });
56
+ // ══════════════════════════════════════════════════════════
57
+ // Circle CCTP V2 Quotes (local calculation + fee API)
58
+ // No deBridge API calls — these are safe from rate limiting
59
+ // ══════════════════════════════════════════════════════════
60
+ describe("CCTP V2 quotes", () => {
61
+ it("solana → arbitrum: forwarding fee (standard finality)", async () => {
62
+ const quote = await getCctpQuote("solana", "arbitrum", 500);
63
+ expect(quote.provider).toBe("cctp");
64
+ expect(quote.fee).toBeLessThan(1); // forwarding ~$0.22
65
+ expect(quote.amountOut).toBeGreaterThan(499);
66
+ expect(quote.estimatedTime).toBeGreaterThan(0);
67
+ });
68
+ it("arbitrum → base: forwarding fee (L2 to L2)", async () => {
69
+ const quote = await getCctpQuote("arbitrum", "base", 200);
70
+ expect(quote.provider).toBe("cctp");
71
+ expect(quote.fee).toBeLessThan(1); // forwarding ~$0.22
72
+ expect(quote.estimatedTime).toBeGreaterThan(0);
73
+ });
74
+ it("base → solana: relay fee (no forwarding for Solana dst)", async () => {
75
+ const quote = await getCctpQuote("base", "solana", 100);
76
+ expect(quote.provider).toBe("cctp");
77
+ expect(quote.fee).toBeLessThan(1);
78
+ expect(quote.estimatedTime).toBeGreaterThan(0);
79
+ });
80
+ it("CCTP quote for tiny amount ($0.01): still valid", async () => {
81
+ const quote = await getCctpQuote("arbitrum", "base", 0.01);
82
+ expect(quote.amountOut).toBeGreaterThan(-1);
83
+ expect(quote.fee).toBeLessThan(1);
84
+ });
85
+ it("getBestQuote selects cheapest provider", async () => {
86
+ const q1 = await getBestQuote("arbitrum", "base", 1000, DUMMY_EVM, DUMMY_EVM);
87
+ expect(["cctp", "relay"]).toContain(q1.provider);
88
+ expect(q1.fee).toBeLessThan(2);
89
+ const q2 = await getBestQuote("solana", "arbitrum", 500, DUMMY_SOLANA, DUMMY_EVM);
90
+ expect(["cctp", "relay"]).toContain(q2.provider);
91
+ expect(q2.fee).toBeLessThan(2);
92
+ });
93
+ it("all CCTP quotes have consistent shape", async () => {
94
+ const routes = [
95
+ ["solana", "arbitrum"],
96
+ ["arbitrum", "base"],
97
+ ["base", "solana"],
98
+ ];
99
+ for (const [src, dst] of routes) {
100
+ const q = await getCctpQuote(src, dst, 100);
101
+ expect(q.provider).toBe("cctp");
102
+ expect(typeof q.srcChain).toBe("string");
103
+ expect(typeof q.dstChain).toBe("string");
104
+ expect(typeof q.amountIn).toBe("number");
105
+ expect(typeof q.amountOut).toBe("number");
106
+ expect(typeof q.fee).toBe("number");
107
+ expect(typeof q.estimatedTime).toBe("number");
108
+ expect(q.amountIn).toBeGreaterThanOrEqual(q.amountOut);
109
+ expect(q.raw).toBeDefined();
110
+ }
111
+ });
112
+ });
113
+ // ══════════════════════════════════════════════════════════
114
+ // deBridge DLN Quote API (real HTTP calls — rate limited)
115
+ // Consolidated into fewer tests to avoid 429 errors
116
+ // ══════════════════════════════════════════════════════════
117
+ describe("deBridge DLN quotes (sequential, rate-limit aware)", () => {
118
+ it("solana → arbitrum: valid quote with fee breakdown", async () => {
119
+ const quote = await getDebridgeQuote("solana", "arbitrum", 100, DUMMY_SOLANA, DUMMY_EVM);
120
+ expect(quote.provider).toBe("debridge");
121
+ expect(quote.srcChain).toBe("solana");
122
+ expect(quote.dstChain).toBe("arbitrum");
123
+ expect(quote.amountIn).toBe(100);
124
+ expect(quote.amountOut).toBeGreaterThan(0);
125
+ expect(quote.amountOut).toBeLessThanOrEqual(100);
126
+ expect(quote.fee).toBeGreaterThanOrEqual(0);
127
+ expect(quote.fee).toBeLessThan(10); // < 10% for $100
128
+ expect(quote.estimatedTime).toBeGreaterThan(0);
129
+ expect(quote.raw).toBeDefined();
130
+ // Verify fee = amountIn - amountOut
131
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
132
+ });
133
+ it("arbitrum → solana: reverse route works", async () => {
134
+ await wait(1500); // respect rate limit
135
+ const quote = await getDebridgeQuote("arbitrum", "solana", 50, DUMMY_EVM, DUMMY_SOLANA);
136
+ expect(quote.provider).toBe("debridge");
137
+ expect(quote.amountIn).toBe(50);
138
+ expect(quote.amountOut).toBeGreaterThan(0);
139
+ expect(quote.amountOut).toBeLessThanOrEqual(50);
140
+ });
141
+ it("EVM-to-EVM route and small amount work", async () => {
142
+ await wait(1500);
143
+ // Test EVM-to-EVM
144
+ const quote = await getDebridgeQuote("base", "arbitrum", 200, DUMMY_EVM, DUMMY_EVM);
145
+ expect(quote.provider).toBe("debridge");
146
+ expect(quote.amountOut).toBeGreaterThan(0);
147
+ expect(quote.estimatedTime).toBeGreaterThan(0);
148
+ });
149
+ it("unsupported chain throws immediately (no API call)", async () => {
150
+ await expect(getDebridgeQuote("fakenet", "arbitrum", 100, DUMMY_EVM, DUMMY_EVM)).rejects.toThrow(/Unsupported chain/i);
151
+ });
152
+ it("zero amount: deBridge rejects", async () => {
153
+ await wait(1500);
154
+ await expect(getDebridgeQuote("solana", "arbitrum", 0, DUMMY_SOLANA, DUMMY_EVM)).rejects.toThrow();
155
+ });
156
+ it("getBestQuote returns valid quote (base → arbitrum)", async () => {
157
+ const quote = await getBestQuote("base", "arbitrum", 100, DUMMY_EVM, DUMMY_EVM);
158
+ expect(["cctp", "relay", "debridge"]).toContain(quote.provider);
159
+ expect(quote.amountOut).toBeGreaterThan(98);
160
+ expect(quote.fee).toBeLessThan(2);
161
+ });
162
+ });
163
+ // ══════════════════════════════════════════════════════════
164
+ // CCTP vs deBridge Comparison (single deBridge call)
165
+ // ══════════════════════════════════════════════════════════
166
+ describe("CCTP vs deBridge comparison", () => {
167
+ it("CCTP is cheaper but slower than deBridge", async () => {
168
+ await wait(2000);
169
+ const cctp = await getCctpQuote("arbitrum", "base", 1000);
170
+ const debridge = await getDebridgeQuote("arbitrum", "base", 1000, DUMMY_EVM, DUMMY_EVM);
171
+ // CCTP is free, deBridge has fees
172
+ expect(cctp.fee).toBeLessThanOrEqual(debridge.fee);
173
+ expect(cctp.amountOut).toBeGreaterThanOrEqual(debridge.amountOut);
174
+ // deBridge is faster (~2s vs CCTP ~60-900s)
175
+ expect(debridge.estimatedTime).toBeLessThan(cctp.estimatedTime);
176
+ // Both have valid shapes
177
+ for (const q of [cctp, debridge]) {
178
+ expect(q.provider).toMatch(/^(cctp|debridge)$/);
179
+ expect(typeof q.srcChain).toBe("string");
180
+ expect(typeof q.dstChain).toBe("string");
181
+ expect(typeof q.amountIn).toBe("number");
182
+ expect(typeof q.amountOut).toBe("number");
183
+ expect(typeof q.fee).toBe("number");
184
+ expect(typeof q.estimatedTime).toBe("number");
185
+ expect(q.raw).toBeDefined();
186
+ }
187
+ });
188
+ });
189
+ // ══════════════════════════════════════════════════════════
190
+ // deBridge Status API (single call)
191
+ // ══════════════════════════════════════════════════════════
192
+ describe("deBridge status check", () => {
193
+ it("non-existent order: returns response or 404", async () => {
194
+ await wait(1500);
195
+ try {
196
+ const status = await checkDebridgeStatus("0x0000000000000000000000000000000000000000000000000000000000000000");
197
+ expect(status).toBeDefined();
198
+ }
199
+ catch (err) {
200
+ expect(String(err)).toMatch(/failed|404|not found|429/i);
201
+ }
202
+ });
203
+ it("invalid order ID format: throws error", async () => {
204
+ await wait(1500);
205
+ try {
206
+ await checkDebridgeStatus("not-a-valid-order-id");
207
+ // If it doesn't throw, it should return something
208
+ }
209
+ catch (err) {
210
+ expect(err).toBeDefined();
211
+ }
212
+ });
213
+ });
214
+ // ══════════════════════════════════════════════════════════
215
+ // Edge Cases (no API calls)
216
+ // ══════════════════════════════════════════════════════════
217
+ describe("edge cases (offline)", () => {
218
+ it("CCTP same-chain doesn't throw", async () => {
219
+ const quote = await getCctpQuote("arbitrum", "arbitrum", 100);
220
+ expect(quote.provider).toBe("cctp");
221
+ expect(quote.fee).toBeLessThan(1);
222
+ expect(quote.amountOut).toBeGreaterThan(99);
223
+ });
224
+ it("CCTP various amounts", async () => {
225
+ for (const amt of [0.01, 1, 100, 1000000]) {
226
+ const q = await getCctpQuote("arbitrum", "base", amt);
227
+ // Forwarding fee ~$0.22
228
+ expect(q.amountOut).toBeGreaterThanOrEqual(amt - 0.50);
229
+ expect(q.fee).toBeLessThan(1);
230
+ }
231
+ });
232
+ it("getBestQuote: all routes return valid cheapest provider", async () => {
233
+ const routes = [
234
+ ["solana", "arbitrum", DUMMY_SOLANA, DUMMY_EVM],
235
+ ["arbitrum", "base", DUMMY_EVM, DUMMY_EVM],
236
+ ["base", "solana", DUMMY_EVM, DUMMY_SOLANA],
237
+ ];
238
+ for (const [src, dst, sender, recipient] of routes) {
239
+ const quote = await getBestQuote(src, dst, 100, sender, recipient);
240
+ expect(["cctp", "relay", "debridge"]).toContain(quote.provider);
241
+ expect(quote.amountIn).toBeGreaterThanOrEqual(quote.amountOut);
242
+ expect(quote.amountOut).toBeGreaterThan(0);
243
+ }
244
+ });
245
+ });
246
+ // ══════════════════════════════════════════════════════════
247
+ // CLI Command Integration (via process spawn)
248
+ // ══════════════════════════════════════════════════════════
249
+ describe("CLI bridge commands", () => {
250
+ const CLI_CWD = "/Users/hik/Documents/GitHub/pacifica/packages/cli";
251
+ const CLI_CMD = "npx tsx src/index.ts";
252
+ function runCliSafe(args) {
253
+ const { execSync } = require("child_process");
254
+ try {
255
+ const stdout = execSync(`${CLI_CMD} ${args}`, {
256
+ encoding: "utf-8",
257
+ cwd: CLI_CWD,
258
+ timeout: 25000,
259
+ env: { ...process.env, NODE_NO_WARNINGS: "1" },
260
+ stdio: ["pipe", "pipe", "pipe"],
261
+ });
262
+ return { stdout, stderr: "", exitCode: 0 };
263
+ }
264
+ catch (err) {
265
+ const e = err;
266
+ return {
267
+ stdout: e.stdout ?? "",
268
+ stderr: e.stderr ?? "",
269
+ exitCode: e.status ?? 1,
270
+ };
271
+ }
272
+ }
273
+ it("bridge chains: returns chain list as JSON", () => {
274
+ const { stdout } = runCliSafe("--json bridge chains");
275
+ const parsed = JSON.parse(stdout);
276
+ expect(parsed.ok).toBe(true);
277
+ expect(parsed.data.chains).toBeDefined();
278
+ expect(parsed.data.usdc).toBeDefined();
279
+ expect(parsed.data.exchanges).toBeDefined();
280
+ expect(parsed.data.chains.solana).toBe(7565164);
281
+ expect(parsed.data.chains.arbitrum).toBe(42161);
282
+ });
283
+ it("bridge chains: text mode has chain names", () => {
284
+ const { stdout } = runCliSafe("bridge chains");
285
+ expect(stdout).toContain("solana");
286
+ expect(stdout).toContain("arbitrum");
287
+ expect(stdout).toContain("ethereum");
288
+ });
289
+ it("bridge quote: CCTP route returns JSON (no deBridge API call)", () => {
290
+ // arbitrum → ethereum uses CCTP, avoids deBridge rate limit
291
+ const { stdout } = runCliSafe("--json bridge quote --from arbitrum --to ethereum --amount 500");
292
+ const parsed = JSON.parse(stdout);
293
+ expect(parsed.ok).toBe(true);
294
+ expect(parsed.data.provider).toBe("cctp");
295
+ expect(parsed.data.srcChain).toBe("arbitrum");
296
+ expect(parsed.data.dstChain).toBe("ethereum");
297
+ expect(parsed.data.amountIn).toBe(500);
298
+ expect(parsed.data.fee).toBeLessThan(1); // forwarding fee ~$0.22
299
+ expect(parsed.data.estimatedTime).toBeGreaterThan(0);
300
+ });
301
+ it("bridge --help lists subcommands", () => {
302
+ const { stdout } = runCliSafe("bridge --help");
303
+ expect(stdout).toContain("chains");
304
+ expect(stdout).toContain("quote");
305
+ expect(stdout).toContain("send");
306
+ expect(stdout).toContain("status");
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,202 @@
1
+ import { execSync } from "child_process";
2
+ import { writeFileSync, unlinkSync, existsSync } from "fs";
3
+ import { describe, it, expect, afterAll } from "vitest";
4
+ const CLI_CWD = "/Users/hik/Documents/GitHub/pacifica/packages/cli";
5
+ const CLI_CMD = "npx tsx src/index.ts";
6
+ /** Temp files created during tests, cleaned up in afterAll */
7
+ const tempFiles = [];
8
+ function runCli(args) {
9
+ return execSync(`${CLI_CMD} ${args}`, {
10
+ encoding: "utf-8",
11
+ cwd: CLI_CWD,
12
+ timeout: 25000,
13
+ env: { ...process.env, NODE_NO_WARNINGS: "1" },
14
+ });
15
+ }
16
+ function runCliSafe(args) {
17
+ try {
18
+ const stdout = execSync(`${CLI_CMD} ${args}`, {
19
+ encoding: "utf-8",
20
+ cwd: CLI_CWD,
21
+ timeout: 25000,
22
+ env: { ...process.env, NODE_NO_WARNINGS: "1" },
23
+ stdio: ["pipe", "pipe", "pipe"],
24
+ });
25
+ return { stdout, stderr: "", exitCode: 0 };
26
+ }
27
+ catch (err) {
28
+ const e = err;
29
+ return {
30
+ stdout: e.stdout ?? "",
31
+ stderr: e.stderr ?? "",
32
+ exitCode: e.status ?? 1,
33
+ };
34
+ }
35
+ }
36
+ function writeTempFile(name, content) {
37
+ const path = `/tmp/${name}`;
38
+ writeFileSync(path, content, "utf-8");
39
+ tempFiles.push(path);
40
+ return path;
41
+ }
42
+ afterAll(() => {
43
+ for (const f of tempFiles) {
44
+ if (existsSync(f)) {
45
+ try {
46
+ unlinkSync(f);
47
+ }
48
+ catch { /* ignore */ }
49
+ }
50
+ }
51
+ });
52
+ describe("CLI E2E Integration Tests", { timeout: 30000 }, () => {
53
+ // ───────────────────── schema command ─────────────────────
54
+ describe("perp schema --json", () => {
55
+ let schema;
56
+ /** schema may be wrapped in envelope { ok, data } or raw */
57
+ function parseSchema(output) {
58
+ const parsed = JSON.parse(output);
59
+ return (parsed.data ?? parsed);
60
+ }
61
+ it("outputs valid JSON with expected top-level structure", () => {
62
+ const output = runCli("schema");
63
+ schema = parseSchema(output);
64
+ expect(schema).toHaveProperty("schemaVersion");
65
+ expect(schema).toHaveProperty("commands");
66
+ expect(schema).toHaveProperty("errorCodes");
67
+ expect(schema).toHaveProperty("exchanges");
68
+ expect(Array.isArray(schema.commands)).toBe(true);
69
+ expect(Array.isArray(schema.exchanges)).toBe(true);
70
+ expect(typeof schema.errorCodes).toBe("object");
71
+ });
72
+ it("commands array contains known command names", () => {
73
+ const output = runCli("schema");
74
+ schema = parseSchema(output);
75
+ const commandNames = schema.commands.map((c) => c.name);
76
+ expect(commandNames).toContain("market");
77
+ expect(commandNames).toContain("account");
78
+ expect(commandNames).toContain("trade");
79
+ expect(commandNames).toContain("arb");
80
+ expect(commandNames).toContain("plan");
81
+ });
82
+ it("errorCodes contains key error types with retryable flags", () => {
83
+ const output = runCli("schema");
84
+ schema = parseSchema(output);
85
+ const errorCodes = schema.errorCodes;
86
+ expect(errorCodes).toHaveProperty("INSUFFICIENT_BALANCE");
87
+ expect(errorCodes.INSUFFICIENT_BALANCE.retryable).toBe(false);
88
+ expect(errorCodes).toHaveProperty("RATE_LIMITED");
89
+ expect(errorCodes.RATE_LIMITED.retryable).toBe(true);
90
+ expect(errorCodes).toHaveProperty("TIMEOUT");
91
+ expect(errorCodes.TIMEOUT.retryable).toBe(true);
92
+ expect(errorCodes).toHaveProperty("EXCHANGE_UNREACHABLE");
93
+ expect(errorCodes.EXCHANGE_UNREACHABLE.retryable).toBe(true);
94
+ expect(errorCodes).toHaveProperty("UNKNOWN");
95
+ expect(errorCodes.UNKNOWN.retryable).toBe(false);
96
+ });
97
+ });
98
+ // ───────────────────── plan commands ─────────────────────
99
+ describe("perp plan example", () => {
100
+ it("outputs valid JSON with version 1.0 and steps array", () => {
101
+ const output = runCli("plan example");
102
+ const parsed = JSON.parse(output);
103
+ // plan example may be wrapped in envelope (ok/data) or raw
104
+ const plan = parsed.data ?? parsed;
105
+ expect(plan.version).toBe("1.0");
106
+ expect(Array.isArray(plan.steps)).toBe(true);
107
+ expect(plan.steps.length).toBeGreaterThan(0);
108
+ // Each step should have id, action, params
109
+ for (const step of plan.steps) {
110
+ expect(step).toHaveProperty("id");
111
+ expect(step).toHaveProperty("action");
112
+ expect(step).toHaveProperty("params");
113
+ }
114
+ });
115
+ });
116
+ describe("perp plan validate", () => {
117
+ it("succeeds for a valid plan (exit 0, output contains 'valid')", () => {
118
+ const validPlan = {
119
+ version: "1.0",
120
+ description: "Test plan",
121
+ steps: [
122
+ {
123
+ id: "step1",
124
+ action: "check_balance",
125
+ params: { minAvailable: 50 },
126
+ onFailure: "abort",
127
+ },
128
+ {
129
+ id: "step2",
130
+ action: "market_order",
131
+ params: { symbol: "ETH", side: "buy", size: "0.1" },
132
+ onFailure: "abort",
133
+ dependsOn: "step1",
134
+ },
135
+ ],
136
+ };
137
+ const filePath = writeTempFile("test-valid-plan.json", JSON.stringify(validPlan, null, 2));
138
+ const { stdout, exitCode } = runCliSafe(`plan validate ${filePath}`);
139
+ expect(exitCode).toBe(0);
140
+ // The human-readable output says "valid" or the JSON output includes valid:true
141
+ const lower = stdout.toLowerCase();
142
+ expect(lower).toContain("valid");
143
+ });
144
+ it("reports errors for an invalid plan (wrong version) with --json", () => {
145
+ const invalidPlan = {
146
+ version: "999.0",
147
+ steps: [
148
+ {
149
+ id: "bad",
150
+ action: "market_order",
151
+ params: {},
152
+ },
153
+ ],
154
+ };
155
+ const filePath = writeTempFile("test-invalid-plan.json", JSON.stringify(invalidPlan, null, 2));
156
+ const { stdout, exitCode } = runCliSafe(`--json plan validate ${filePath}`);
157
+ // Should still exit 0 because validation itself succeeds (reports errors in JSON)
158
+ expect(exitCode).toBe(0);
159
+ const parsed = JSON.parse(stdout);
160
+ expect(parsed.ok).toBe(true);
161
+ // The data.valid should be false
162
+ expect(parsed.data.valid).toBe(false);
163
+ expect(Array.isArray(parsed.data.errors)).toBe(true);
164
+ expect(parsed.data.errors.length).toBeGreaterThan(0);
165
+ // Should mention version mismatch
166
+ const allErrors = parsed.data.errors.join(" ");
167
+ expect(allErrors).toContain("version");
168
+ });
169
+ });
170
+ // ───────────────────── --json error wrapping ─────────────────────
171
+ describe("--json structured error output", () => {
172
+ it("outputs structured JSON error for a nonexistent plan file", () => {
173
+ // Use plan validate with a file that does not exist — this triggers
174
+ // withJsonErrors which wraps the ENOENT in the standard envelope.
175
+ const { stdout, exitCode } = runCliSafe("--json plan validate /tmp/__nonexistent_cli_test_file_99999.json");
176
+ expect(exitCode).toBe(0);
177
+ const parsed = JSON.parse(stdout);
178
+ expect(parsed.ok).toBe(false);
179
+ expect(parsed.error).toBeDefined();
180
+ expect(typeof parsed.error.code).toBe("string");
181
+ expect(typeof parsed.error.message).toBe("string");
182
+ expect(parsed.error.message).toContain("ENOENT");
183
+ expect(typeof parsed.error.retryable).toBe("boolean");
184
+ expect(parsed).toHaveProperty("meta");
185
+ expect(parsed.meta).toHaveProperty("timestamp");
186
+ });
187
+ });
188
+ // ───────────────────── help output ─────────────────────
189
+ describe("perp --help", () => {
190
+ it("includes all major commands in help output", () => {
191
+ const { stdout } = runCliSafe("--help");
192
+ const helpText = stdout.toLowerCase();
193
+ expect(helpText).toContain("schema");
194
+ expect(helpText).toContain("plan");
195
+ expect(helpText).toContain("trade");
196
+ expect(helpText).toContain("stream");
197
+ expect(helpText).toContain("market");
198
+ expect(helpText).toContain("account");
199
+ expect(helpText).toContain("arb");
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { fetchAllDexAssets, findDexArbPairs, scanDexArb } from "../../dex-asset-map.js";
3
+ /**
4
+ * Integration tests for HIP-3 cross-dex arb scanning.
5
+ *
6
+ * Hits real Hyperliquid mainnet API (read-only, no private key needed).
7
+ *
8
+ * Run: pnpm --filter perp-cli test -- --testPathPattern dex-arb.integration
9
+ */
10
+ describe("fetchAllDexAssets — live API", () => {
11
+ it("returns assets from native HL and at least 3 deployed dexes", async () => {
12
+ const assets = await fetchAllDexAssets();
13
+ expect(assets.length).toBeGreaterThan(100);
14
+ // Check we have native HL assets
15
+ const hlAssets = assets.filter(a => a.dex === "hl");
16
+ expect(hlAssets.length).toBeGreaterThan(100);
17
+ expect(hlAssets.find(a => a.base === "BTC")).toBeTruthy();
18
+ // Check we have at least xyz dex
19
+ const xyzAssets = assets.filter(a => a.dex === "xyz");
20
+ expect(xyzAssets.length).toBeGreaterThan(10);
21
+ expect(xyzAssets.find(a => a.base === "TSLA")).toBeTruthy();
22
+ // Verify data shape
23
+ for (const asset of assets.slice(0, 10)) {
24
+ expect(asset.markPrice).toBeGreaterThan(0);
25
+ expect(typeof asset.fundingRate).toBe("number");
26
+ expect(asset.dex).toBeTruthy();
27
+ expect(asset.base).toBeTruthy();
28
+ expect(asset.raw).toBeTruthy();
29
+ }
30
+ const dexes = new Set(assets.map(a => a.dex));
31
+ console.log(`Dexes found: ${[...dexes].join(", ")} (${dexes.size} total)`);
32
+ console.log(`Total active assets: ${assets.length}`);
33
+ }, 30000);
34
+ });
35
+ describe("findDexArbPairs — live data", () => {
36
+ it("finds TSLA arb pairs across dexes", async () => {
37
+ const assets = await fetchAllDexAssets();
38
+ const tslaAssets = assets.filter(a => a.base === "TSLA");
39
+ expect(tslaAssets.length).toBeGreaterThanOrEqual(2);
40
+ const pairs = findDexArbPairs(tslaAssets);
41
+ // C(n,2) pairs if n dexes have TSLA
42
+ expect(pairs.length).toBeGreaterThanOrEqual(1);
43
+ for (const p of pairs) {
44
+ expect(p.underlying).toBe("TSLA");
45
+ expect(p.priceGapPct).toBeLessThan(2); // prices should be very close
46
+ expect(p.long.dex).not.toBe(p.short.dex);
47
+ }
48
+ const dexes = new Set(tslaAssets.map(a => a.dex));
49
+ console.log(`TSLA on dexes: ${[...dexes].join(", ")} → ${pairs.length} arb pairs`);
50
+ for (const p of pairs.slice(0, 3)) {
51
+ console.log(` L:${p.long.dex} S:${p.short.dex} spread:${p.annualSpread.toFixed(1)}% gap:${p.priceGapPct.toFixed(3)}%`);
52
+ }
53
+ }, 30000);
54
+ it("finds BTC arb between hl native and hyna dex", async () => {
55
+ const assets = await fetchAllDexAssets();
56
+ const btcAssets = assets.filter(a => a.base === "BTC");
57
+ const hlBTC = btcAssets.find(a => a.dex === "hl");
58
+ const hynaBTC = btcAssets.find(a => a.dex === "hyna");
59
+ expect(hlBTC).toBeTruthy();
60
+ expect(hynaBTC).toBeTruthy();
61
+ // Prices should be very close (< 0.5%)
62
+ const gap = Math.abs(hlBTC.markPrice - hynaBTC.markPrice) / hlBTC.markPrice * 100;
63
+ expect(gap).toBeLessThan(0.5);
64
+ console.log(`BTC: hl=$${hlBTC.markPrice.toFixed(0)} hyna=$${hynaBTC.markPrice.toFixed(0)} gap:${gap.toFixed(4)}%`);
65
+ }, 30000);
66
+ it("correctly rejects USAR vs US500 (different products)", async () => {
67
+ const assets = await fetchAllDexAssets();
68
+ const usar = assets.find(a => a.base === "USAR");
69
+ const us500 = assets.find(a => a.base === "US500");
70
+ if (usar && us500) {
71
+ // Prices should be wildly different
72
+ const gap = Math.abs(usar.markPrice - us500.markPrice) / Math.min(usar.markPrice, us500.markPrice) * 100;
73
+ expect(gap).toBeGreaterThan(100); // way more than 5%
74
+ // findDexArbPairs should not match these
75
+ const pairs = findDexArbPairs([usar, us500]);
76
+ expect(pairs).toHaveLength(0);
77
+ console.log(`USAR=$${usar.markPrice.toFixed(2)} vs US500=$${us500.markPrice.toFixed(2)} → gap:${gap.toFixed(0)}% → correctly rejected`);
78
+ }
79
+ }, 30000);
80
+ });
81
+ describe("scanDexArb — live full scan", () => {
82
+ it("returns sorted arb opportunities", async () => {
83
+ const pairs = await scanDexArb({ minAnnualSpread: 5 });
84
+ expect(pairs.length).toBeGreaterThan(0);
85
+ // Should be sorted by annualSpread descending
86
+ for (let i = 1; i < pairs.length; i++) {
87
+ expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
88
+ }
89
+ // All pairs should have different dexes
90
+ for (const p of pairs) {
91
+ expect(p.long.dex).not.toBe(p.short.dex);
92
+ }
93
+ // All pairs should have < 5% price gap
94
+ for (const p of pairs) {
95
+ expect(p.priceGapPct).toBeLessThan(5);
96
+ }
97
+ console.log(`Found ${pairs.length} arb opportunities (>5% annual spread)`);
98
+ console.log(`Top 5:`);
99
+ for (const p of pairs.slice(0, 5)) {
100
+ console.log(` ${p.underlying}: ${p.annualSpread.toFixed(1)}% [${p.long.dex}↔${p.short.dex}]`);
101
+ }
102
+ }, 30000);
103
+ it("no-native mode excludes HL base assets", async () => {
104
+ const withNative = await scanDexArb({ minAnnualSpread: 0, includeNative: true });
105
+ const withoutNative = await scanDexArb({ minAnnualSpread: 0, includeNative: false });
106
+ // Without native, should have fewer pairs (no hl↔dex pairs)
107
+ const nativePairs = withNative.filter(p => p.long.dex === "hl" || p.short.dex === "hl");
108
+ const nonNativePairs = withoutNative.filter(p => p.long.dex === "hl" || p.short.dex === "hl");
109
+ expect(nonNativePairs.length).toBe(0);
110
+ if (nativePairs.length > 0) {
111
+ expect(withNative.length).toBeGreaterThan(withoutNative.length);
112
+ }
113
+ console.log(`With native: ${withNative.length} pairs (${nativePairs.length} include HL native)`);
114
+ console.log(`Without native: ${withoutNative.length} pairs`);
115
+ }, 30000);
116
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Integration tests verifying JSON envelope consistency across all CLI commands.
3
+ *
4
+ * Every --json output must:
5
+ * 1. Be valid JSON (single object, no extra text)
6
+ * 2. Have ok: boolean
7
+ * 3. If ok=true: have data and meta.timestamp
8
+ * 4. If ok=false: have error.code, error.message, and meta.timestamp
9
+ *
10
+ * These tests spawn the real CLI process to catch any console.log leaks,
11
+ * chalk output in JSON mode, or missing envelope wrappers.
12
+ */
13
+ import "dotenv/config";