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,812 @@
1
+ /**
2
+ * Strict Bridge Integration Tests — comprehensive verification using real keys.
3
+ *
4
+ * Uses .env keys for address derivation and balance checks.
5
+ * NO transactions are executed. NO funds are spent.
6
+ *
7
+ * Core verification: "sender pays only" — all bridge fees are deducted from
8
+ * the source USDC amount. The recipient receives amountOut without paying
9
+ * any gas or fees on the destination chain.
10
+ *
11
+ * Tests:
12
+ * 1. Key derivation & address consistency
13
+ * 2. Real balance checks on all chains
14
+ * 3. CCTP quote API for all routes (including HyperCore)
15
+ * 4. HyperCore CCTP fees API validation
16
+ * 5. deBridge quote API with real addresses
17
+ * 6. Relay quote API with real addresses
18
+ * 7. getAllQuotes aggregation correctness
19
+ * 8. getBestQuote selection logic
20
+ * 9. Cross-provider quote comparison
21
+ * 10. Edge cases and error handling
22
+ * 11-14. On-chain / API verification
23
+ * 15. SENDER-PAYS-ONLY: full 4-chain matrix verification
24
+ * 16. Complete route matrix (all 12 directional pairs)
25
+ */
26
+ import { describe, it, expect, beforeAll } from "vitest";
27
+ import { config } from "dotenv";
28
+ import { CHAIN_IDS, USDC_ADDRESSES, CCTP_DOMAINS, EXCHANGE_TO_CHAIN, getCctpQuote, getDebridgeQuote, getRelayQuote, getAllQuotes, getBestQuote, getEvmUsdcBalance, getSolanaUsdcBalance, checkBridgeBalance, getNativeGasBalance, checkBridgeGasBalance, } from "../../bridge-engine.js";
29
+ // Load .env
30
+ config();
31
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
32
+ // ── Derive real addresses from .env keys ──
33
+ let solanaAddress;
34
+ let evmAddress;
35
+ describe("Strict Bridge Integration Tests", { timeout: 120000 }, () => {
36
+ // ══════════════════════════════════════════════════════════
37
+ // 0. Setup: derive addresses from private keys
38
+ // ══════════════════════════════════════════════════════════
39
+ beforeAll(async () => {
40
+ // Derive Solana address
41
+ const { Keypair } = await import("@solana/web3.js");
42
+ const bs58 = await import("bs58");
43
+ const solanaKey = process.env.pk ?? process.env.PACIFICA_PRIVATE_KEY;
44
+ if (!solanaKey)
45
+ throw new Error("Missing 'pk' or 'PACIFICA_PRIVATE_KEY' in .env");
46
+ const keypair = Keypair.fromSecretKey(bs58.default.decode(solanaKey));
47
+ solanaAddress = keypair.publicKey.toBase58();
48
+ // Derive EVM address
49
+ const { ethers } = await import("ethers");
50
+ const evmKey = process.env.HL_PRIVATE_KEY;
51
+ if (!evmKey)
52
+ throw new Error("Missing 'HL_PRIVATE_KEY' in .env");
53
+ const wallet = new ethers.Wallet(evmKey);
54
+ evmAddress = wallet.address;
55
+ });
56
+ // ══════════════════════════════════════════════════════════
57
+ // 1. Key & Address Validation
58
+ // ══════════════════════════════════════════════════════════
59
+ describe("1. Key derivation & address format", () => {
60
+ it("Solana address is valid base58 (32-44 chars)", () => {
61
+ expect(solanaAddress).toBeDefined();
62
+ expect(solanaAddress.length).toBeGreaterThanOrEqual(32);
63
+ expect(solanaAddress.length).toBeLessThanOrEqual(44);
64
+ expect(solanaAddress).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);
65
+ });
66
+ it("EVM address is valid checksummed address", () => {
67
+ expect(evmAddress).toBeDefined();
68
+ expect(evmAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
69
+ });
70
+ it("HL_PRIVATE_KEY and LIGHTER_PRIVATE_KEY derive same EVM address", async () => {
71
+ const { ethers } = await import("ethers");
72
+ const hlWallet = new ethers.Wallet(process.env.HL_PRIVATE_KEY);
73
+ const lighterWallet = new ethers.Wallet(process.env.LIGHTER_PRIVATE_KEY);
74
+ expect(hlWallet.address).toBe(lighterWallet.address);
75
+ });
76
+ });
77
+ // ══════════════════════════════════════════════════════════
78
+ // 2. Configuration Consistency
79
+ // ══════════════════════════════════════════════════════════
80
+ describe("2. Configuration consistency", () => {
81
+ it("EXCHANGE_TO_CHAIN maps correctly", () => {
82
+ expect(EXCHANGE_TO_CHAIN.pacifica).toBe("solana");
83
+ expect(EXCHANGE_TO_CHAIN.hyperliquid).toBe("hyperliquid");
84
+ expect(EXCHANGE_TO_CHAIN.lighter).toBe("arbitrum");
85
+ });
86
+ it("all standard chains have chain IDs", () => {
87
+ expect(CHAIN_IDS.solana).toBe(7565164);
88
+ expect(CHAIN_IDS.arbitrum).toBe(42161);
89
+ expect(CHAIN_IDS.base).toBe(8453);
90
+ });
91
+ it("all standard chains have USDC addresses", () => {
92
+ for (const chain of ["solana", "arbitrum", "base"]) {
93
+ expect(USDC_ADDRESSES[chain]).toBeDefined();
94
+ }
95
+ });
96
+ it("CCTP domains include hyperliquid (19)", () => {
97
+ expect(CCTP_DOMAINS.solana).toBe(5);
98
+ expect(CCTP_DOMAINS.arbitrum).toBe(3);
99
+ expect(CCTP_DOMAINS.base).toBe(6);
100
+ expect(CCTP_DOMAINS.hyperliquid).toBe(19);
101
+ });
102
+ it("no unsupported chains leak into config", () => {
103
+ const supportedChains = ["solana", "arbitrum", "base"];
104
+ for (const chain of Object.keys(CHAIN_IDS)) {
105
+ expect(supportedChains).toContain(chain);
106
+ }
107
+ });
108
+ it("CCTP domains only contain supported chains + hyperliquid", () => {
109
+ const allowed = ["solana", "arbitrum", "base", "hyperliquid"];
110
+ for (const chain of Object.keys(CCTP_DOMAINS)) {
111
+ expect(allowed).toContain(chain);
112
+ }
113
+ });
114
+ });
115
+ // ══════════════════════════════════════════════════════════
116
+ // 3. Real Balance Checks
117
+ // ══════════════════════════════════════════════════════════
118
+ describe("3. Real balance checks", () => {
119
+ it("Solana USDC balance query succeeds", async () => {
120
+ const balance = await getSolanaUsdcBalance(solanaAddress);
121
+ expect(typeof balance).toBe("number");
122
+ expect(balance).toBeGreaterThanOrEqual(0);
123
+ });
124
+ it("Arbitrum USDC balance query succeeds", async () => {
125
+ const balance = await getEvmUsdcBalance("arbitrum", evmAddress);
126
+ expect(typeof balance).toBe("number");
127
+ expect(balance).toBeGreaterThanOrEqual(0);
128
+ });
129
+ it("Base USDC balance query succeeds", async () => {
130
+ const balance = await getEvmUsdcBalance("base", evmAddress);
131
+ expect(typeof balance).toBe("number");
132
+ expect(balance).toBeGreaterThanOrEqual(0);
133
+ });
134
+ it("checkBridgeBalance returns consistent shape", async () => {
135
+ const result = await checkBridgeBalance("solana", solanaAddress, 1);
136
+ expect(typeof result.balance).toBe("number");
137
+ expect(typeof result.sufficient).toBe("boolean");
138
+ expect(result.sufficient).toBe(result.balance >= 1);
139
+ });
140
+ it("unsupported chain balance throws", async () => {
141
+ await expect(getEvmUsdcBalance("fakenet", evmAddress)).rejects.toThrow();
142
+ });
143
+ });
144
+ // ══════════════════════════════════════════════════════════
145
+ // 3b. Native Gas Balance Checks
146
+ // ══════════════════════════════════════════════════════════
147
+ describe("3b. Native gas balance checks", () => {
148
+ it("Solana SOL balance query succeeds", async () => {
149
+ const balance = await getNativeGasBalance("solana", solanaAddress);
150
+ expect(typeof balance).toBe("number");
151
+ expect(balance).toBeGreaterThanOrEqual(0);
152
+ });
153
+ it("Arbitrum ETH balance query succeeds", async () => {
154
+ const balance = await getNativeGasBalance("arbitrum", evmAddress);
155
+ expect(typeof balance).toBe("number");
156
+ expect(balance).toBeGreaterThanOrEqual(0);
157
+ });
158
+ it("Base ETH balance query succeeds", async () => {
159
+ const balance = await getNativeGasBalance("base", evmAddress);
160
+ expect(typeof balance).toBe("number");
161
+ expect(balance).toBeGreaterThanOrEqual(0);
162
+ });
163
+ it("unsupported chain gas balance throws", async () => {
164
+ await expect(getNativeGasBalance("fakenet", evmAddress)).rejects.toThrow();
165
+ });
166
+ it("checkBridgeGasBalance: src-only check (fast mode)", async () => {
167
+ const result = await checkBridgeGasBalance("arbitrum", evmAddress, "base", evmAddress, false);
168
+ expect(typeof result.ok).toBe("boolean");
169
+ expect(Array.isArray(result.errors)).toBe(true);
170
+ });
171
+ it("checkBridgeGasBalance: src+dst check (standard mode)", async () => {
172
+ const result = await checkBridgeGasBalance("arbitrum", evmAddress, "base", evmAddress, true);
173
+ expect(typeof result.ok).toBe("boolean");
174
+ expect(Array.isArray(result.errors)).toBe(true);
175
+ });
176
+ it("checkBridgeGasBalance: returns errors array for insufficient gas", async () => {
177
+ // Use an address unlikely to have gas on both chains
178
+ const emptyAddr = "0x000000000000000000000000000000000000dEaD";
179
+ const result = await checkBridgeGasBalance("arbitrum", emptyAddr, "base", emptyAddr, true);
180
+ // Shape is correct regardless of balance
181
+ expect(typeof result.ok).toBe("boolean");
182
+ expect(Array.isArray(result.errors)).toBe(true);
183
+ if (!result.ok) {
184
+ expect(result.errors[0]).toMatch(/arbitrum|base/);
185
+ }
186
+ });
187
+ });
188
+ // ══════════════════════════════════════════════════════════
189
+ // 4. CCTP Quotes — Standard Routes
190
+ // ══════════════════════════════════════════════════════════
191
+ describe("4. CCTP standard quotes", () => {
192
+ const standardRoutes = [
193
+ ["solana", "arbitrum"],
194
+ ["solana", "base"],
195
+ ["arbitrum", "base"],
196
+ ["arbitrum", "solana"],
197
+ ["base", "arbitrum"],
198
+ ["base", "solana"],
199
+ ];
200
+ for (const [src, dst] of standardRoutes) {
201
+ it(`${src} → ${dst}: valid CCTP quote`, async () => {
202
+ const quote = await getCctpQuote(src, dst, 100);
203
+ expect(quote.provider).toBe("cctp");
204
+ expect(quote.srcChain).toBe(src);
205
+ expect(quote.dstChain).toBe(dst);
206
+ expect(quote.amountIn).toBe(100);
207
+ expect(quote.amountOut).toBeGreaterThan(0);
208
+ expect(quote.amountOut).toBeLessThanOrEqual(100);
209
+ expect(quote.fee).toBeGreaterThanOrEqual(0);
210
+ expect(quote.fee).toBeLessThan(5); // max $5 relay fee
211
+ expect(quote.estimatedTime).toBeGreaterThan(0);
212
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
213
+ expect(quote.raw).toBeDefined();
214
+ });
215
+ }
216
+ it("large amount ($100k) quote is valid", async () => {
217
+ const quote = await getCctpQuote("arbitrum", "base", 100000);
218
+ expect(quote.amountOut).toBeGreaterThan(99900); // fee should be tiny relative to amount
219
+ expect(quote.fee).toBeLessThan(5);
220
+ });
221
+ it("tiny amount ($0.01) quote is valid", async () => {
222
+ const quote = await getCctpQuote("arbitrum", "base", 0.01);
223
+ expect(quote.amountOut).toBeGreaterThan(-1);
224
+ expect(quote.fee).toBeLessThan(1);
225
+ });
226
+ });
227
+ // ══════════════════════════════════════════════════════════
228
+ // 5. CCTP Quotes — HyperCore Routes
229
+ // ══════════════════════════════════════════════════════════
230
+ describe("5. HyperCore CCTP quotes", () => {
231
+ it("solana → hyperliquid: valid HyperCore deposit quote", async () => {
232
+ const quote = await getCctpQuote("solana", "hyperliquid", 500);
233
+ expect(quote.provider).toBe("cctp");
234
+ expect(quote.srcChain).toBe("solana");
235
+ expect(quote.dstChain).toBe("hyperliquid");
236
+ expect(quote.amountIn).toBe(500);
237
+ expect(quote.amountOut).toBeGreaterThan(0);
238
+ expect(quote.amountOut).toBeLessThan(500);
239
+ expect(quote.fee).toBeGreaterThan(0);
240
+ expect(quote.fee).toBeLessThan(5); // should be ~$0.25 (1bp + forwarding)
241
+ expect(quote.estimatedTime).toBe(65); // Solana source
242
+ expect(quote.gasIncluded).toBe(true);
243
+ expect(quote.raw).toHaveProperty("type", "cctp-hypercore");
244
+ expect(quote.raw).toHaveProperty("maxFee");
245
+ });
246
+ it("arbitrum → hyperliquid: valid HyperCore deposit quote", async () => {
247
+ const quote = await getCctpQuote("arbitrum", "hyperliquid", 1000);
248
+ expect(quote.provider).toBe("cctp");
249
+ expect(quote.srcChain).toBe("arbitrum");
250
+ expect(quote.dstChain).toBe("hyperliquid");
251
+ expect(quote.amountIn).toBe(1000);
252
+ expect(quote.amountOut).toBeGreaterThan(0);
253
+ expect(quote.fee).toBeGreaterThan(0);
254
+ expect(quote.fee).toBeLessThan(5);
255
+ expect(quote.estimatedTime).toBe(60); // EVM-to-EVM fast
256
+ expect(quote.gasIncluded).toBe(true);
257
+ expect(quote.raw).toHaveProperty("type", "cctp-hypercore");
258
+ });
259
+ it("base → hyperliquid: valid HyperCore deposit quote", async () => {
260
+ const quote = await getCctpQuote("base", "hyperliquid", 200);
261
+ expect(quote.provider).toBe("cctp");
262
+ expect(quote.dstChain).toBe("hyperliquid");
263
+ expect(quote.amountOut).toBeGreaterThan(0);
264
+ expect(quote.fee).toBeLessThan(5);
265
+ expect(quote.estimatedTime).toBe(60);
266
+ });
267
+ it("hyperliquid → arbitrum: valid HyperCore withdrawal quote", async () => {
268
+ const quote = await getCctpQuote("hyperliquid", "arbitrum", 300);
269
+ expect(quote.provider).toBe("cctp");
270
+ expect(quote.srcChain).toBe("hyperliquid");
271
+ expect(quote.dstChain).toBe("arbitrum");
272
+ expect(quote.amountIn).toBe(300);
273
+ expect(quote.amountOut).toBe(299.80); // $0.20 forwarding fee
274
+ expect(quote.fee).toBe(0.20);
275
+ expect(quote.estimatedTime).toBe(60);
276
+ expect(quote.gasIncluded).toBe(true);
277
+ expect(quote.raw).toHaveProperty("type", "cctp-hypercore-withdraw");
278
+ });
279
+ it("hyperliquid → solana: valid HyperCore withdrawal quote", async () => {
280
+ const quote = await getCctpQuote("hyperliquid", "solana", 100);
281
+ expect(quote.provider).toBe("cctp");
282
+ expect(quote.srcChain).toBe("hyperliquid");
283
+ expect(quote.dstChain).toBe("solana");
284
+ expect(quote.fee).toBe(0.20);
285
+ expect(quote.amountOut).toBe(99.80);
286
+ });
287
+ it("HyperCore fees scale correctly with amount", async () => {
288
+ const small = await getCctpQuote("arbitrum", "hyperliquid", 10);
289
+ const large = await getCctpQuote("arbitrum", "hyperliquid", 10000);
290
+ // Protocol fee is 1bp, so large amount pays more
291
+ expect(large.fee).toBeGreaterThan(small.fee);
292
+ // But fee percentage should be similar (both ~1bp + flat forwarding)
293
+ const smallPct = small.fee / small.amountIn;
294
+ const largePct = large.fee / large.amountIn;
295
+ expect(smallPct).toBeGreaterThan(largePct); // smaller amounts pay higher % (flat fee dominates)
296
+ });
297
+ });
298
+ // ══════════════════════════════════════════════════════════
299
+ // 6. HyperCore Fees API Direct Validation
300
+ // ══════════════════════════════════════════════════════════
301
+ describe("6. HyperCore fees API", () => {
302
+ const FEES_API = "https://iris-api.circle.com/v2/burn/USDC/fees";
303
+ it("Solana (domain 5) → HyperCore (domain 19) fees API responds", async () => {
304
+ const res = await fetch(`${FEES_API}/5/19?forward=true&hyperCoreDeposit=true`);
305
+ expect(res.ok).toBe(true);
306
+ const data = await res.json();
307
+ expect(Array.isArray(data)).toBe(true);
308
+ expect(data.length).toBeGreaterThan(0);
309
+ const schedule = data[0];
310
+ expect(schedule).toHaveProperty("finalityThreshold");
311
+ expect(schedule).toHaveProperty("minimumFee");
312
+ });
313
+ it("Arbitrum (domain 3) → HyperCore (domain 19) fees API responds", async () => {
314
+ const res = await fetch(`${FEES_API}/3/19?forward=true&hyperCoreDeposit=true`);
315
+ expect(res.ok).toBe(true);
316
+ const data = await res.json();
317
+ expect(Array.isArray(data)).toBe(true);
318
+ expect(data.length).toBeGreaterThan(0);
319
+ });
320
+ it("Base (domain 6) → HyperCore (domain 19) fees API responds", async () => {
321
+ const res = await fetch(`${FEES_API}/6/19?forward=true&hyperCoreDeposit=true`);
322
+ expect(res.ok).toBe(true);
323
+ const data = await res.json();
324
+ expect(Array.isArray(data)).toBe(true);
325
+ });
326
+ it("fees API returns forward fee structure", async () => {
327
+ const res = await fetch(`${FEES_API}/3/19?forward=true&hyperCoreDeposit=true`);
328
+ const data = await res.json();
329
+ // Look for schedule with forwardFee
330
+ const hasForwardFee = data.some(s => s.forwardFee !== undefined && s.forwardFee !== null);
331
+ // At minimum, all schedules should have finalityThreshold and minimumFee
332
+ for (const schedule of data) {
333
+ expect(typeof schedule.finalityThreshold).toBe("number");
334
+ expect(typeof schedule.minimumFee).toBe("number");
335
+ }
336
+ // Forward fee may or may not be present depending on API version
337
+ if (hasForwardFee) {
338
+ const withFee = data.find(s => s.forwardFee !== undefined);
339
+ const ff = withFee.forwardFee;
340
+ expect(typeof ff.low).toBe("number");
341
+ expect(typeof ff.med).toBe("number");
342
+ expect(typeof ff.high).toBe("number");
343
+ expect(ff.med).toBeGreaterThanOrEqual(ff.low);
344
+ expect(ff.high).toBeGreaterThanOrEqual(ff.med);
345
+ }
346
+ });
347
+ });
348
+ // ══════════════════════════════════════════════════════════
349
+ // 7. deBridge DLN Quotes (with real addresses)
350
+ // ══════════════════════════════════════════════════════════
351
+ describe("7. deBridge quotes with real addresses", () => {
352
+ it("solana → arbitrum: quote with real wallet addresses", async () => {
353
+ const quote = await getDebridgeQuote("solana", "arbitrum", 100, solanaAddress, evmAddress);
354
+ expect(quote.provider).toBe("debridge");
355
+ expect(quote.srcChain).toBe("solana");
356
+ expect(quote.dstChain).toBe("arbitrum");
357
+ expect(quote.amountIn).toBe(100);
358
+ expect(quote.amountOut).toBeGreaterThan(0);
359
+ expect(quote.amountOut).toBeLessThanOrEqual(100);
360
+ expect(quote.fee).toBeGreaterThanOrEqual(0);
361
+ expect(quote.estimatedTime).toBeGreaterThan(0);
362
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
363
+ });
364
+ it("arbitrum → base: EVM-to-EVM with real addresses", async () => {
365
+ await wait(1500);
366
+ const quote = await getDebridgeQuote("arbitrum", "base", 200, evmAddress, evmAddress);
367
+ expect(quote.provider).toBe("debridge");
368
+ expect(quote.amountOut).toBeGreaterThan(0);
369
+ expect(quote.amountOut).toBeLessThanOrEqual(200);
370
+ });
371
+ it("base → solana: reverse route with real addresses", async () => {
372
+ await wait(1500);
373
+ const quote = await getDebridgeQuote("base", "solana", 50, evmAddress, solanaAddress);
374
+ expect(quote.provider).toBe("debridge");
375
+ expect(quote.amountOut).toBeGreaterThan(0);
376
+ });
377
+ it("unsupported chain throws", async () => {
378
+ await expect(getDebridgeQuote("hyperliquid", "arbitrum", 100, evmAddress, evmAddress)).rejects.toThrow(/Unsupported chain/i);
379
+ });
380
+ });
381
+ // ══════════════════════════════════════════════════════════
382
+ // 8. Relay Quotes (with real addresses)
383
+ // ══════════════════════════════════════════════════════════
384
+ describe("8. Relay quotes with real addresses", () => {
385
+ it("solana → arbitrum: Relay quote with real addresses", async () => {
386
+ const quote = await getRelayQuote("solana", "arbitrum", 100, solanaAddress, evmAddress);
387
+ expect(quote.provider).toBe("relay");
388
+ expect(quote.srcChain).toBe("solana");
389
+ expect(quote.dstChain).toBe("arbitrum");
390
+ expect(quote.amountIn).toBe(100);
391
+ expect(quote.amountOut).toBeGreaterThan(0);
392
+ expect(quote.amountOut).toBeLessThanOrEqual(100);
393
+ expect(quote.estimatedTime).toBeGreaterThan(0);
394
+ });
395
+ it("arbitrum → base: EVM-to-EVM Relay quote", async () => {
396
+ await wait(1000);
397
+ const quote = await getRelayQuote("arbitrum", "base", 500, evmAddress, evmAddress);
398
+ expect(quote.provider).toBe("relay");
399
+ expect(quote.amountOut).toBeGreaterThan(0);
400
+ });
401
+ it("base → solana: reverse Relay quote", async () => {
402
+ await wait(1000);
403
+ const quote = await getRelayQuote("base", "solana", 200, evmAddress, solanaAddress);
404
+ expect(quote.provider).toBe("relay");
405
+ expect(quote.amountOut).toBeGreaterThan(0);
406
+ });
407
+ });
408
+ // ══════════════════════════════════════════════════════════
409
+ // 9. getAllQuotes Aggregation
410
+ // ══════════════════════════════════════════════════════════
411
+ describe("9. getAllQuotes aggregation", () => {
412
+ it("arbitrum → base: returns multiple providers sorted by amountOut", async () => {
413
+ await wait(2000);
414
+ const quotes = await getAllQuotes("arbitrum", "base", 1000, evmAddress, evmAddress);
415
+ expect(quotes.length).toBeGreaterThanOrEqual(1);
416
+ // Should be sorted by amountOut descending (best first)
417
+ for (let i = 1; i < quotes.length; i++) {
418
+ expect(quotes[i - 1].amountOut).toBeGreaterThanOrEqual(quotes[i].amountOut);
419
+ }
420
+ // All quotes should have consistent shape
421
+ for (const q of quotes) {
422
+ expect(["cctp", "debridge", "relay"]).toContain(q.provider);
423
+ expect(q.srcChain).toBe("arbitrum");
424
+ expect(q.dstChain).toBe("base");
425
+ expect(q.amountIn).toBe(1000);
426
+ expect(q.amountOut).toBeGreaterThan(0);
427
+ expect(typeof q.fee).toBe("number");
428
+ expect(typeof q.estimatedTime).toBe("number");
429
+ }
430
+ // CCTP should be present (it's always available for supported routes)
431
+ const cctpQuote = quotes.find(q => q.provider === "cctp");
432
+ expect(cctpQuote).toBeDefined();
433
+ });
434
+ it("solana → arbitrum: includes CCTP + at least one other provider", async () => {
435
+ await wait(2000);
436
+ const quotes = await getAllQuotes("solana", "arbitrum", 200, solanaAddress, evmAddress);
437
+ expect(quotes.length).toBeGreaterThanOrEqual(2);
438
+ const providers = new Set(quotes.map(q => q.provider));
439
+ expect(providers.has("cctp")).toBe(true);
440
+ // Should have debridge or relay too
441
+ expect(providers.size).toBeGreaterThanOrEqual(2);
442
+ });
443
+ });
444
+ // ══════════════════════════════════════════════════════════
445
+ // 10. getBestQuote Selection
446
+ // ══════════════════════════════════════════════════════════
447
+ describe("10. getBestQuote selection", () => {
448
+ it("best quote is cheapest provider for standard chains", async () => {
449
+ const quote = await getBestQuote("arbitrum", "base", 500, evmAddress, evmAddress);
450
+ // Best quote should be either CCTP or Relay (both valid, sorted by amountOut)
451
+ expect(["cctp", "relay"]).toContain(quote.provider);
452
+ expect(quote.amountOut).toBeGreaterThanOrEqual(498);
453
+ expect(quote.fee).toBeLessThan(2);
454
+ });
455
+ it("solana → arbitrum: CCTP preferred", async () => {
456
+ const quote = await getBestQuote("solana", "arbitrum", 1000, solanaAddress, evmAddress);
457
+ expect(quote.provider).toBe("cctp");
458
+ expect(quote.fee).toBeLessThan(2);
459
+ });
460
+ it("quote amountIn - amountOut ≈ fee", async () => {
461
+ const quote = await getBestQuote("base", "solana", 100, evmAddress, solanaAddress);
462
+ const calculatedFee = quote.amountIn - quote.amountOut;
463
+ expect(Math.abs(calculatedFee - quote.fee)).toBeLessThan(0.01);
464
+ });
465
+ });
466
+ // ══════════════════════════════════════════════════════════
467
+ // 11. Cross-Provider Comparison
468
+ // ══════════════════════════════════════════════════════════
469
+ describe("11. Cross-provider fee comparison", () => {
470
+ it("CCTP is cheaper than deBridge for standard routes", async () => {
471
+ await wait(2000);
472
+ const cctp = await getCctpQuote("arbitrum", "base", 1000);
473
+ const debridge = await getDebridgeQuote("arbitrum", "base", 1000, evmAddress, evmAddress);
474
+ expect(cctp.fee).toBeLessThanOrEqual(debridge.fee);
475
+ expect(cctp.amountOut).toBeGreaterThanOrEqual(debridge.amountOut);
476
+ });
477
+ it("HyperCore CCTP fee is reasonable ($0.20-$2 range)", async () => {
478
+ const quote = await getCctpQuote("arbitrum", "hyperliquid", 1000);
479
+ expect(quote.fee).toBeGreaterThanOrEqual(0.10);
480
+ expect(quote.fee).toBeLessThan(3);
481
+ });
482
+ it("HyperCore withdrawal fee is fixed at $0.20", async () => {
483
+ const q100 = await getCctpQuote("hyperliquid", "arbitrum", 100);
484
+ const q10000 = await getCctpQuote("hyperliquid", "arbitrum", 10000);
485
+ expect(q100.fee).toBe(0.20);
486
+ expect(q10000.fee).toBe(0.20);
487
+ });
488
+ });
489
+ // ══════════════════════════════════════════════════════════
490
+ // 12. Error Handling & Edge Cases
491
+ // ══════════════════════════════════════════════════════════
492
+ describe("12. Error handling", () => {
493
+ it("CCTP quote for unsupported chain throws", async () => {
494
+ await expect(getCctpQuote("polygon", "arbitrum", 100)).rejects.toThrow(/not supported/i);
495
+ });
496
+ it("deBridge zero amount throws", async () => {
497
+ await wait(1500);
498
+ await expect(getDebridgeQuote("solana", "arbitrum", 0, solanaAddress, evmAddress)).rejects.toThrow();
499
+ });
500
+ it("deBridge negative amount throws", async () => {
501
+ await wait(1500);
502
+ await expect(getDebridgeQuote("arbitrum", "base", -100, evmAddress, evmAddress)).rejects.toThrow();
503
+ });
504
+ it("CCTP same-chain quote still valid", async () => {
505
+ const quote = await getCctpQuote("arbitrum", "arbitrum", 100);
506
+ expect(quote.provider).toBe("cctp");
507
+ expect(quote.amountOut).toBeGreaterThan(0);
508
+ });
509
+ it("all quotes return gasIncluded field", async () => {
510
+ const cctp = await getCctpQuote("arbitrum", "base", 100);
511
+ expect(typeof cctp.gasIncluded).toBe("boolean");
512
+ const hypercore = await getCctpQuote("arbitrum", "hyperliquid", 100);
513
+ expect(hypercore.gasIncluded).toBe(true);
514
+ });
515
+ });
516
+ // ══════════════════════════════════════════════════════════
517
+ // 13. HyperCore CctpForwarder Contract Verification
518
+ // ══════════════════════════════════════════════════════════
519
+ describe("13. HyperCore on-chain verification", () => {
520
+ const HYPERCORE_FORWARDER = "0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757";
521
+ const HYPERCORE_RPC = "https://rpc.hyperliquid.xyz/evm";
522
+ it("CctpForwarder contract exists on HyperEVM", async () => {
523
+ const res = await fetch(HYPERCORE_RPC, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({
527
+ jsonrpc: "2.0", id: 1,
528
+ method: "eth_getCode",
529
+ params: [HYPERCORE_FORWARDER, "latest"],
530
+ }),
531
+ });
532
+ const json = await res.json();
533
+ // Contract should have code (not "0x")
534
+ expect(json.result).toBeDefined();
535
+ expect(json.result.length).toBeGreaterThan(2);
536
+ expect(json.result).not.toBe("0x");
537
+ });
538
+ it("CctpExtension contract exists on Arbitrum", async () => {
539
+ const CCTP_EXTENSION = "0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE";
540
+ const res = await fetch("https://arb1.arbitrum.io/rpc", {
541
+ method: "POST",
542
+ headers: { "Content-Type": "application/json" },
543
+ body: JSON.stringify({
544
+ jsonrpc: "2.0", id: 1,
545
+ method: "eth_getCode",
546
+ params: [CCTP_EXTENSION, "latest"],
547
+ }),
548
+ });
549
+ const json = await res.json();
550
+ expect(json.result.length).toBeGreaterThan(2);
551
+ expect(json.result).not.toBe("0x");
552
+ });
553
+ it("HyperEVM RPC is reachable (eth_chainId)", async () => {
554
+ const res = await fetch(HYPERCORE_RPC, {
555
+ method: "POST",
556
+ headers: { "Content-Type": "application/json" },
557
+ body: JSON.stringify({
558
+ jsonrpc: "2.0", id: 1,
559
+ method: "eth_chainId",
560
+ params: [],
561
+ }),
562
+ });
563
+ const json = await res.json();
564
+ expect(json.result).toBeDefined();
565
+ // HyperEVM chain ID = 999 (0x3e7) or similar
566
+ const chainId = parseInt(json.result, 16);
567
+ expect(chainId).toBeGreaterThan(0);
568
+ });
569
+ });
570
+ // ══════════════════════════════════════════════════════════
571
+ // 14. CCTP V2 Iris Attestation API
572
+ // ══════════════════════════════════════════════════════════
573
+ describe("14. CCTP V2 Iris attestation API", () => {
574
+ it("V2 relay fee API responds for all source domains", async () => {
575
+ const routes = [
576
+ [5, 3], // solana → arbitrum
577
+ [3, 6], // arbitrum → base
578
+ [6, 5], // base → solana
579
+ [3, 19], // arbitrum → hyperliquid
580
+ [5, 19], // solana → hyperliquid
581
+ ];
582
+ for (const [src, dst] of routes) {
583
+ const res = await fetch(`https://iris-api.circle.com/v2/burn/USDC/fees/${src}/${dst}`);
584
+ expect(res.ok).toBe(true);
585
+ const data = await res.json();
586
+ expect(Array.isArray(data)).toBe(true);
587
+ }
588
+ });
589
+ });
590
+ // ══════════════════════════════════════════════════════════
591
+ // 15. SENDER-PAYS-ONLY — Core Invariant
592
+ // ══════════════════════════════════════════════════════════
593
+ //
594
+ // The fundamental rule: ALL bridge costs (protocol fee, relay fee,
595
+ // gas cost) are deducted from the sender's USDC amount.
596
+ // The recipient receives amountOut without paying anything.
597
+ //
598
+ // This is verified by checking that every quote has:
599
+ // - gasIncluded === true (auto-relay, no manual dst TX needed)
600
+ // - fee === amountIn - amountOut (fee is deducted from sent amount)
601
+ // - amountOut > 0 and amountOut <= amountIn
602
+ //
603
+ // ══════════════════════════════════════════════════════════
604
+ describe("15. SENDER-PAYS-ONLY verification", () => {
605
+ // All 4 chains: arbitrum, base, solana, hyperliquid
606
+ // Standard routes (6 pairs among arb/base/sol)
607
+ const standardRoutes = [
608
+ ["arbitrum", "base"],
609
+ ["arbitrum", "solana"],
610
+ ["base", "arbitrum"],
611
+ ["base", "solana"],
612
+ ["solana", "arbitrum"],
613
+ ["solana", "base"],
614
+ ];
615
+ // HyperCore routes (deposit into + withdrawal from)
616
+ const hyperCoreDepositRoutes = [
617
+ ["arbitrum", "hyperliquid"],
618
+ ["base", "hyperliquid"],
619
+ ["solana", "hyperliquid"],
620
+ ];
621
+ const hyperCoreWithdrawRoutes = [
622
+ ["hyperliquid", "arbitrum"],
623
+ ["hyperliquid", "base"],
624
+ ["hyperliquid", "solana"],
625
+ ];
626
+ const ALL_ROUTES = [
627
+ ...standardRoutes,
628
+ ...hyperCoreDepositRoutes,
629
+ ...hyperCoreWithdrawRoutes,
630
+ ];
631
+ for (const [src, dst] of ALL_ROUTES) {
632
+ it(`${src} → ${dst}: fee deducted from sender's USDC`, async () => {
633
+ const amount = 100;
634
+ const quote = await getCctpQuote(src, dst, amount);
635
+ // 1. Fee is correctly calculated
636
+ expect(quote.fee).toBeGreaterThanOrEqual(0);
637
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
638
+ // 2. Recipient gets amountOut
639
+ expect(quote.amountOut).toBeGreaterThan(0);
640
+ expect(quote.amountOut).toBeLessThanOrEqual(amount);
641
+ // 3. Gas note present
642
+ expect(quote.gasNote).toBeDefined();
643
+ expect(quote.gasNote.length).toBeGreaterThan(0);
644
+ });
645
+ }
646
+ it("fast=true: all standard routes use auto-relay (gasIncluded=true)", async () => {
647
+ for (const [src, dst] of standardRoutes) {
648
+ const quote = await getCctpQuote(src, dst, 100, true);
649
+ expect(quote.gasIncluded).toBe(true);
650
+ const raw = quote.raw;
651
+ expect(Number(raw.maxFee)).toBeGreaterThan(0);
652
+ expect(raw.fast).toBe(true);
653
+ }
654
+ });
655
+ it("fast=false (default): forwarding routes have gasIncluded=true, Solana dst has gasIncluded=false", async () => {
656
+ for (const [src, dst] of standardRoutes) {
657
+ const quote = await getCctpQuote(src, dst, 100);
658
+ const raw = quote.raw;
659
+ expect(raw.fast).toBe(false);
660
+ // Forwarding available for all routes except →Solana
661
+ if (dst === "solana") {
662
+ expect(quote.gasIncluded).toBe(false);
663
+ expect(raw.forwarding).toBe(false);
664
+ }
665
+ else {
666
+ expect(quote.gasIncluded).toBe(true);
667
+ expect(raw.forwarding).toBe(true);
668
+ }
669
+ }
670
+ });
671
+ it("fast=true fees are higher than standard fees", async () => {
672
+ const standard = await getCctpQuote("arbitrum", "base", 1000, false);
673
+ const fast = await getCctpQuote("arbitrum", "base", 1000, true);
674
+ expect(fast.fee).toBeGreaterThan(standard.fee);
675
+ expect(fast.estimatedTime).toBeLessThan(standard.estimatedTime);
676
+ });
677
+ it("HyperCore deposits: CctpForwarder auto-deposits (always gasIncluded)", async () => {
678
+ for (const [src, dst] of hyperCoreDepositRoutes) {
679
+ const quote = await getCctpQuote(src, dst, 100);
680
+ expect(quote.gasIncluded).toBe(true);
681
+ expect(quote.gasNote).toContain("CctpForwarder");
682
+ const raw = quote.raw;
683
+ expect(raw.type).toBe("cctp-hypercore");
684
+ expect(Number(raw.maxFee)).toBeGreaterThan(0);
685
+ }
686
+ });
687
+ it("HyperCore withdrawals: HL handles forwarding (always gasIncluded)", async () => {
688
+ for (const [src, dst] of hyperCoreWithdrawRoutes) {
689
+ const quote = await getCctpQuote(src, dst, 100);
690
+ expect(quote.gasIncluded).toBe(true);
691
+ expect(quote.gasNote).toContain("HyperCore");
692
+ expect(quote.fee).toBe(0.20);
693
+ const raw = quote.raw;
694
+ expect(raw.type).toBe("cctp-hypercore-withdraw");
695
+ }
696
+ });
697
+ it("deBridge quotes include gas in fee (sender-pays)", async () => {
698
+ await wait(2000);
699
+ const quote = await getDebridgeQuote("arbitrum", "base", 100, evmAddress, evmAddress);
700
+ expect(quote.fee).toBeGreaterThan(0);
701
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
702
+ expect(quote.gasIncluded).toBe(true);
703
+ });
704
+ it("Relay quotes include gas in fee (solver pays dst gas)", async () => {
705
+ await wait(1000);
706
+ const quote = await getRelayQuote("arbitrum", "base", 100, evmAddress, evmAddress);
707
+ expect(quote.fee).toBeGreaterThanOrEqual(0);
708
+ expect(Math.abs(quote.fee - (quote.amountIn - quote.amountOut))).toBeLessThan(0.001);
709
+ expect(quote.gasIncluded).toBe(true);
710
+ });
711
+ it("getAllQuotes: fee math is correct for every provider", async () => {
712
+ await wait(2000);
713
+ const quotes = await getAllQuotes("arbitrum", "base", 500, evmAddress, evmAddress);
714
+ for (const q of quotes) {
715
+ expect(q.fee).toBeGreaterThanOrEqual(0);
716
+ expect(Math.abs(q.fee - (q.amountIn - q.amountOut))).toBeLessThan(0.01);
717
+ }
718
+ });
719
+ });
720
+ // ══════════════════════════════════════════════════════════
721
+ // 16. Complete 4-Chain Route Matrix
722
+ // ══════════════════════════════════════════════════════════
723
+ //
724
+ // Verify every possible directional pair among:
725
+ // arbitrum, base, solana, hyperliquid (12 pairs total)
726
+ //
727
+ // For each: valid quote, correct fee math, gasIncluded
728
+ // ══════════════════════════════════════════════════════════
729
+ describe("16. Complete 4-chain route matrix", () => {
730
+ const chains = ["arbitrum", "base", "solana", "hyperliquid"];
731
+ const amount = 250;
732
+ for (const src of chains) {
733
+ for (const dst of chains) {
734
+ if (src === dst)
735
+ continue;
736
+ it(`[MATRIX] ${src} → ${dst}: valid quote with sender-pays`, async () => {
737
+ const quote = await getCctpQuote(src, dst, amount);
738
+ // Basic shape
739
+ expect(quote.provider).toBe("cctp");
740
+ expect(quote.srcChain).toBe(src);
741
+ expect(quote.dstChain).toBe(dst);
742
+ expect(quote.amountIn).toBe(amount);
743
+ expect(quote.amountOut).toBeGreaterThan(0);
744
+ expect(quote.amountOut).toBeLessThanOrEqual(amount);
745
+ // Fee invariant: fee = amountIn - amountOut
746
+ expect(Math.abs(quote.fee - (amount - quote.amountOut))).toBeLessThan(0.001);
747
+ // gasIncluded: true for HyperCore, forwarding (EVM dst), and fast
748
+ // Only →Solana in standard mode has gasIncluded=false (no forwarding)
749
+ if (dst === "solana" && src !== "hyperliquid") {
750
+ expect(quote.gasIncluded).toBe(false);
751
+ }
752
+ else {
753
+ expect(quote.gasIncluded).toBe(true);
754
+ }
755
+ // Reasonable fee (< $5 for $250)
756
+ expect(quote.fee).toBeLessThan(5);
757
+ // Time estimate is positive
758
+ expect(quote.estimatedTime).toBeGreaterThan(0);
759
+ });
760
+ }
761
+ }
762
+ it("[MATRIX] fee comparison: HyperCore deposit > standard CCTP > HyperCore withdrawal", async () => {
763
+ const standardFee = (await getCctpQuote("arbitrum", "base", 1000)).fee;
764
+ const depositFee = (await getCctpQuote("arbitrum", "hyperliquid", 1000)).fee;
765
+ const withdrawFee = (await getCctpQuote("hyperliquid", "arbitrum", 1000)).fee;
766
+ // HyperCore deposit has protocol + forwarding fee (> standard)
767
+ expect(depositFee).toBeGreaterThanOrEqual(standardFee);
768
+ // HyperCore withdrawal is fixed $0.20
769
+ expect(withdrawFee).toBe(0.20);
770
+ });
771
+ it("[MATRIX] ETA comparison: fast mode Solana routes are slower than fast EVM-only", async () => {
772
+ const evmToEvmFast = (await getCctpQuote("arbitrum", "base", 100, true)).estimatedTime;
773
+ const solToEvmFast = (await getCctpQuote("solana", "arbitrum", 100, true)).estimatedTime;
774
+ // Fast finality: Solana ~90s, EVM-only ~60s
775
+ expect(solToEvmFast).toBeGreaterThanOrEqual(evmToEvmFast);
776
+ // Standard finality: both positive
777
+ const evmToEvmStd = (await getCctpQuote("arbitrum", "base", 100)).estimatedTime;
778
+ const solToEvmStd = (await getCctpQuote("solana", "arbitrum", 100)).estimatedTime;
779
+ expect(evmToEvmStd).toBeGreaterThan(0);
780
+ expect(solToEvmStd).toBeGreaterThan(0);
781
+ });
782
+ it("[MATRIX] all standard relay fee APIs respond for 4-chain matrix", async () => {
783
+ // Verify Circle's fee API works for every domain pair we support
784
+ const domains = [
785
+ { chain: "arbitrum", domain: 3 },
786
+ { chain: "base", domain: 6 },
787
+ { chain: "solana", domain: 5 },
788
+ { chain: "hyperliquid", domain: 19 },
789
+ ];
790
+ const results = [];
791
+ for (const src of domains) {
792
+ for (const dst of domains) {
793
+ if (src.chain === dst.chain)
794
+ continue;
795
+ const url = dst.domain === 19
796
+ ? `https://iris-api.circle.com/v2/burn/USDC/fees/${src.domain}/${dst.domain}?forward=true&hyperCoreDeposit=true`
797
+ : `https://iris-api.circle.com/v2/burn/USDC/fees/${src.domain}/${dst.domain}`;
798
+ const res = await fetch(url);
799
+ if (res.ok) {
800
+ results.push(`${src.chain}→${dst.chain}: OK`);
801
+ }
802
+ else {
803
+ results.push(`${src.chain}→${dst.chain}: ${res.status}`);
804
+ }
805
+ }
806
+ }
807
+ // At minimum, all standard routes should work
808
+ const okCount = results.filter(r => r.includes("OK")).length;
809
+ expect(okCount).toBeGreaterThanOrEqual(6); // at least standard 6 pairs
810
+ });
811
+ });
812
+ });