perp-cli 0.12.17 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,646 @@
1
+ /**
2
+ * Hyperliquid Outcome Adapter (HIP-4).
3
+ *
4
+ * Composes with HyperliquidAdapter for signing. Asset id encoding:
5
+ * 100,000,000 + (10 * outcome + side)
6
+ *
7
+ * Coin name conventions:
8
+ * - l2Book/candle/allMids: `#<encoding>` (e.g., `#10`)
9
+ * - spotClearinghouseState balance: `+<encoding>` (e.g., `+10`)
10
+ *
11
+ * Outcome trades draw collateral from the spot balance of the outcome's own quote token —
12
+ * `outcomeMeta.outcomes[].quoteToken` (live 2026-10-12: "USDC" on every listed outcome; a USDH-only
13
+ * balance was refused by the venue "Insufficient spot balance asset=100014881"). Min order value:
14
+ * 10 quote tokens (docs error-responses MinTradeSpotNtl "Order must have minimum value of 10 {quote_token}.").
15
+ */
16
+ import { PerpError } from "../errors.js";
17
+ const OUTCOME_ASSET_OFFSET = 100_000_000;
18
+ /** docs error-responses MinTradeSpotNtl: "Order must have minimum value of 10 {quote_token}." */
19
+ const MIN_ORDER_QUOTE = 10;
20
+ /** Outcome encoding `10*outcome + side` keeps side as the units digit, so
21
+ * side must be 0..9 to avoid overflow into the next outcome's id space. */
22
+ const MAX_SIDE = 9;
23
+ /** Asset id offset spans `[100_000_000, 200_000_000)`, so encoding < 100M. */
24
+ const MAX_ENCODING = 99_999_999;
25
+ export class HyperliquidOutcomeAdapter {
26
+ name = "hyperliquid";
27
+ _hl;
28
+ _outcomeMeta = null;
29
+ _initialized = false;
30
+ constructor(hlAdapter) {
31
+ this._hl = hlAdapter;
32
+ }
33
+ async init() {
34
+ if (this._initialized)
35
+ return;
36
+ await this._loadOutcomeMeta();
37
+ this._initialized = true;
38
+ }
39
+ static encoding(outcome, side) {
40
+ return 10 * outcome + side;
41
+ }
42
+ static assetId(outcome, side) {
43
+ return OUTCOME_ASSET_OFFSET + HyperliquidOutcomeAdapter.encoding(outcome, side);
44
+ }
45
+ static mintCoin(outcome, side) {
46
+ return `#${HyperliquidOutcomeAdapter.encoding(outcome, side)}`;
47
+ }
48
+ static balanceCoin(outcome, side) {
49
+ return `+${HyperliquidOutcomeAdapter.encoding(outcome, side)}`;
50
+ }
51
+ /**
52
+ * Decode a balance coin name like `+10` back to (outcome, side).
53
+ * Returns null if the name does not match the outcome scheme.
54
+ */
55
+ static decodeBalanceCoin(coin) {
56
+ if (!coin.startsWith("+"))
57
+ return null;
58
+ const enc = Number(coin.slice(1));
59
+ if (!Number.isFinite(enc) || enc < 0)
60
+ return null;
61
+ return { outcome: Math.floor(enc / 10), side: enc % 10 };
62
+ }
63
+ static parseDescription(description) {
64
+ const parts = description.split("|");
65
+ const out = {};
66
+ for (const p of parts) {
67
+ const idx = p.indexOf(":");
68
+ if (idx < 0)
69
+ continue;
70
+ const key = p.slice(0, idx).trim();
71
+ const val = p.slice(idx + 1).trim();
72
+ if (key === "class")
73
+ out.class = val;
74
+ else if (key === "underlying")
75
+ out.underlying = val;
76
+ else if (key === "expiry")
77
+ out.expiryMs = HyperliquidOutcomeAdapter._parseExpiry(val);
78
+ else if (key === "targetPrice")
79
+ out.targetPrice = Number(val);
80
+ else if (key === "period")
81
+ out.period = val;
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Compute the underlying mark-price view (gap / inTheMoney) for an outcome
87
+ * from a parsed description and the live allMids map.
88
+ *
89
+ * Pure helper — extracted from getView so the settlement-status logic is
90
+ * directly unit-testable.
91
+ *
92
+ * - HL `allMids` keys perps by bare symbol (e.g. "BTC"). HIP-3 perps use
93
+ * "@dexIdx:SYMBOL" but those aren't referenced in HIP-4 outcomes yet.
94
+ * - For `class:priceBinary` the convention is Yes = "underlying >= target".
95
+ * When `class` is missing or non-binary, `inTheMoney` stays null rather
96
+ * than guessing (Rule #2 — no silent classification fallback).
97
+ * - Returns null when there is no underlying field to look up.
98
+ */
99
+ static _computeUnderlying(parsed, allMids) {
100
+ if (!parsed.underlying)
101
+ return null;
102
+ const sym = parsed.underlying.toUpperCase();
103
+ const markPrice = allMids[sym];
104
+ const target = parsed.targetPrice;
105
+ let gap;
106
+ let gapPct;
107
+ let inTheMoney = null;
108
+ if (markPrice !== undefined && target !== undefined) {
109
+ gap = Number(markPrice) - target;
110
+ gapPct = (gap / target) * 100;
111
+ if (parsed.class === "priceBinary" && Number.isFinite(gap)) {
112
+ inTheMoney = gap >= 0 ? "yes" : "no";
113
+ }
114
+ }
115
+ return {
116
+ symbol: sym,
117
+ source: sym,
118
+ markPrice,
119
+ targetPrice: target,
120
+ gap,
121
+ gapPct,
122
+ inTheMoney,
123
+ };
124
+ }
125
+ /**
126
+ * Sum of `impliedProb` across sides — for fair binary markets the sum
127
+ * should converge to ~1.0. Deviation hints at arbitrage or stale mids.
128
+ *
129
+ * Returns undefined when any side is missing impliedProb OR when any
130
+ * impliedProb is non-finite (NaN, Infinity). This means "we don't have a
131
+ * trustworthy view of the symmetry right now" rather than emitting NaN
132
+ * downstream (Rule #2 — no silent garbage propagation).
133
+ */
134
+ static _computeMidSum(sides) {
135
+ if (sides.length === 0)
136
+ return undefined;
137
+ for (const s of sides) {
138
+ if (s.impliedProb === undefined)
139
+ return undefined;
140
+ if (!Number.isFinite(s.impliedProb))
141
+ return undefined;
142
+ }
143
+ return sides.reduce((acc, s) => acc + (s.impliedProb ?? 0), 0);
144
+ }
145
+ /**
146
+ * Compute the time-status pair (`serverTime`, `msToExpiry`) for a view.
147
+ * Pure helper — takes `nowMs` as an argument so callers can inject a
148
+ * deterministic clock under test.
149
+ *
150
+ * `msToExpiry` is the raw signed delta `expiryMs - nowMs`:
151
+ * positive = unexpired
152
+ * zero = at expiry
153
+ * negative = already settled (caller decides UX)
154
+ * undefined = unknown expiry
155
+ *
156
+ * Does NOT clamp negatives or treat them as "expired" — that
157
+ * classification is the caller's job (Rule #2 — no silent classification
158
+ * fallback in a low-level helper).
159
+ */
160
+ static _computeTimeStatus(expiryMs, nowMs) {
161
+ return {
162
+ serverTime: nowMs,
163
+ msToExpiry: expiryMs !== undefined ? expiryMs - nowMs : undefined,
164
+ };
165
+ }
166
+ /**
167
+ * Pure arithmetic gate for the (outcome, side) pair.
168
+ *
169
+ * Rejects NaN / non-integer / negative values immediately so the
170
+ * encoding formula `10 * outcome + side` never produces a garbage
171
+ * asset id silently. Does NOT consult outcomeMeta — that lookup is in
172
+ * the instance-level `_validateOutcomeSide` which composes this
173
+ * helper with the live registry check.
174
+ *
175
+ * Boundary: outcome=9_999_999, side=9 → encoding=99_999_999 = MAX_ENCODING (valid).
176
+ * outcome=10_000_000, side=0 → encoding=100_000_000 > MAX_ENCODING (rejected).
177
+ */
178
+ static _assertOutcomeRange(outcome, side) {
179
+ if (!Number.isInteger(outcome) || outcome < 0) {
180
+ throw new PerpError("INVALID_PARAMS", `Outcome id must be a non-negative integer, got: ${outcome}`, { exchange: "hyperliquid" });
181
+ }
182
+ if (!Number.isInteger(side) || side < 0 || side > MAX_SIDE) {
183
+ throw new PerpError("INVALID_PARAMS", `Side must be an integer 0..${MAX_SIDE} (encoding scheme is single digit), got: ${side}`, { exchange: "hyperliquid" });
184
+ }
185
+ const encoding = HyperliquidOutcomeAdapter.encoding(outcome, side);
186
+ if (encoding > MAX_ENCODING) {
187
+ throw new PerpError("INVALID_PARAMS", `Encoding ${encoding} overflows the outcome asset block (max ${MAX_ENCODING})`, { exchange: "hyperliquid" });
188
+ }
189
+ }
190
+ /**
191
+ * Trim a raw orderbook to `depth` levels and surface best bid/ask.
192
+ *
193
+ * Throws (rather than silently coercing) when:
194
+ * - `book.bids` or `book.asks` is missing/non-array (venue payload
195
+ * malformed — Rule #2: don't fabricate an empty book)
196
+ * - `depth` is not a non-negative integer (NaN, negative, Infinity,
197
+ * fractional are all caller bugs that previously silently produced
198
+ * `slice(0, NaN) === []` or `slice(0, -1)` = "all but last")
199
+ */
200
+ static _trimBook(book, depth) {
201
+ if (!book || !Array.isArray(book.bids) || !Array.isArray(book.asks)) {
202
+ throw new PerpError("EXCHANGE_ERROR", "Outcome orderbook response is missing bids/asks array", { exchange: "hyperliquid" });
203
+ }
204
+ if (!Number.isInteger(depth) || depth < 0) {
205
+ throw new PerpError("INVALID_PARAMS", `Depth must be a non-negative integer, got: ${depth}`, { exchange: "hyperliquid" });
206
+ }
207
+ const bids = book.bids.slice(0, depth);
208
+ const asks = book.asks.slice(0, depth);
209
+ return {
210
+ bids,
211
+ asks,
212
+ bestBid: bids[0]?.[0],
213
+ bestAsk: asks[0]?.[0],
214
+ };
215
+ }
216
+ /** Parse "20260504-0600" → ms-epoch (UTC). Returns undefined for malformed. */
217
+ static _parseExpiry(s) {
218
+ const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(s);
219
+ if (!m)
220
+ return undefined;
221
+ const [, y, mo, d, h, mi] = m;
222
+ const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi));
223
+ return Number.isFinite(ms) ? ms : undefined;
224
+ }
225
+ async _loadOutcomeMeta() {
226
+ const meta = await this._infoPost({ type: "outcomeMeta" });
227
+ if (!meta || typeof meta !== "object" || !("outcomes" in meta)) {
228
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid outcomeMeta returned unexpected shape: ${JSON.stringify(meta).slice(0, 200)}`, { exchange: "hyperliquid" });
229
+ }
230
+ this._outcomeMeta = meta;
231
+ }
232
+ async getMarkets() {
233
+ await this.init();
234
+ if (!this._outcomeMeta)
235
+ return [];
236
+ const allMids = await this._infoPost({ type: "allMids" });
237
+ return this._outcomeMeta.outcomes.map((o) => {
238
+ const parsed = HyperliquidOutcomeAdapter.parseDescription(o.description);
239
+ const sides = o.sideSpecs.map((s, i) => {
240
+ const encoding = HyperliquidOutcomeAdapter.encoding(o.outcome, i);
241
+ return {
242
+ side: i,
243
+ name: s.name,
244
+ encoding,
245
+ assetId: OUTCOME_ASSET_OFFSET + encoding,
246
+ mid: allMids[`#${encoding}`],
247
+ };
248
+ });
249
+ return {
250
+ outcome: o.outcome,
251
+ quoteToken: this.quoteTokenOf(o.outcome),
252
+ name: o.name,
253
+ description: o.description,
254
+ ...parsed,
255
+ sides,
256
+ };
257
+ });
258
+ }
259
+ async getPositions() {
260
+ await this.init();
261
+ // Bypass HL adapter's cached spot state — outcome positions change at
262
+ // every fill, and the spot cache TTL would surface stale data.
263
+ const userAddress = await this._resolveUserAddress();
264
+ const state = await this._infoPost({ type: "spotClearinghouseState", user: userAddress });
265
+ const balances = (state?.balances ?? []);
266
+ const allMidsPromise = this._infoPost({ type: "allMids" });
267
+ const positions = [];
268
+ for (const b of balances) {
269
+ const coin = String(b.coin ?? "");
270
+ const decoded = HyperliquidOutcomeAdapter.decodeBalanceCoin(coin);
271
+ if (!decoded)
272
+ continue;
273
+ const total = String(b.total ?? "0");
274
+ if (Number(total) === 0)
275
+ continue;
276
+ const sideName = this._lookupSideName(decoded.outcome, decoded.side);
277
+ positions.push({
278
+ outcome: decoded.outcome,
279
+ side: decoded.side,
280
+ sideName,
281
+ encoding: HyperliquidOutcomeAdapter.encoding(decoded.outcome, decoded.side),
282
+ size: total,
283
+ entryNotional: String(b.entryNtl ?? "0"),
284
+ });
285
+ }
286
+ if (positions.length === 0)
287
+ return [];
288
+ const allMids = await allMidsPromise;
289
+ for (const p of positions) {
290
+ const mark = allMids[`#${p.encoding}`];
291
+ if (mark) {
292
+ p.markPrice = mark;
293
+ p.unrealizedPnl = String(Number(mark) * Number(p.size) - Number(p.entryNotional));
294
+ }
295
+ }
296
+ return positions;
297
+ }
298
+ /**
299
+ * Assemble a combined view of one outcome: all sides' books in parallel,
300
+ * the underlying mark price (HL perp mid for `description.underlying`),
301
+ * gap vs targetPrice, time-to-expiry, and per-side implied probability.
302
+ *
303
+ * Outcome markets have a symmetric (Yes / No) structure where the prices
304
+ * sum to ~$1; this view exposes both sides in a single round-trip and
305
+ * also surfaces the directional context for binary markets — what BTC
306
+ * mark price would settle the contract right now.
307
+ */
308
+ async getView(outcome, depth = 10) {
309
+ await this.init();
310
+ const meta = this._outcomeMeta?.outcomes.find((o) => o.outcome === outcome);
311
+ if (!meta) {
312
+ throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, {
313
+ exchange: "hyperliquid",
314
+ remediation: "Run: perp outcome list",
315
+ });
316
+ }
317
+ const parsed = HyperliquidOutcomeAdapter.parseDescription(meta.description);
318
+ // Fetch books for every side in parallel + allMids once for mids and
319
+ // the underlying symbol's mark price.
320
+ const allMidsPromise = this._infoPost({ type: "allMids" });
321
+ const bookPromises = meta.sideSpecs.map((_, i) => this.getOrderbook(outcome, i));
322
+ const [allMids, ...books] = await Promise.all([allMidsPromise, ...bookPromises]);
323
+ // Trim each book to `depth` levels and compute best bid/ask + implied prob.
324
+ const sides = meta.sideSpecs.map((spec, i) => {
325
+ const encoding = HyperliquidOutcomeAdapter.encoding(outcome, i);
326
+ const trimmed = HyperliquidOutcomeAdapter._trimBook(books[i], depth);
327
+ const mid = allMids[`#${encoding}`];
328
+ return {
329
+ side: i,
330
+ name: spec.name,
331
+ encoding,
332
+ assetId: OUTCOME_ASSET_OFFSET + encoding,
333
+ mid,
334
+ bids: trimmed.bids,
335
+ asks: trimmed.asks,
336
+ bestBid: trimmed.bestBid,
337
+ bestAsk: trimmed.bestAsk,
338
+ impliedProb: mid !== undefined ? Number(mid) : undefined,
339
+ };
340
+ });
341
+ const midSum = HyperliquidOutcomeAdapter._computeMidSum(sides);
342
+ // Underlying: HL perp mid for the parsed underlying symbol.
343
+ const underlying = HyperliquidOutcomeAdapter._computeUnderlying(parsed, allMids);
344
+ const { serverTime, msToExpiry } = HyperliquidOutcomeAdapter._computeTimeStatus(parsed.expiryMs, Date.now());
345
+ return {
346
+ outcome,
347
+ name: meta.name,
348
+ description: meta.description,
349
+ class: parsed.class,
350
+ expiryMs: parsed.expiryMs,
351
+ msToExpiry,
352
+ period: parsed.period,
353
+ underlying,
354
+ sides,
355
+ midSum,
356
+ serverTime,
357
+ };
358
+ }
359
+ async getOrderbook(outcome, side) {
360
+ await this.init();
361
+ this._validateOutcomeSide(outcome, side);
362
+ const coin = HyperliquidOutcomeAdapter.mintCoin(outcome, side);
363
+ const book = await this._infoPost({ type: "l2Book", coin });
364
+ // Rule #2: do NOT fabricate an empty book when the venue payload is
365
+ // malformed. Caller (typically getView) has its own gates downstream
366
+ // but a missing `levels` here is a venue contract break, not "no
367
+ // resting orders".
368
+ if (!book ||
369
+ !Array.isArray(book.levels) ||
370
+ !Array.isArray(book.levels[0]) ||
371
+ !Array.isArray(book.levels[1])) {
372
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned malformed payload for ${coin}: missing or non-array \`levels\``, { exchange: "hyperliquid" });
373
+ }
374
+ const levels = book.levels;
375
+ // Rule #2: `Number("abc") ?? 0` would silently coerce a non-numeric
376
+ // venue time to 0 (1970 epoch). 0 is a legitimate "no time given"
377
+ // value when book.time is undefined, but a NaN should not be hidden.
378
+ //
379
+ // Strict policy (qa/2026-05-16): empty string is corruption too —
380
+ // `Number("") === 0` would land in the same fake-1970-epoch bucket
381
+ // as the previous silent path. Treat "" the same as a non-numeric
382
+ // string, distinct from the legitimate undefined/null "no time given".
383
+ // The SDK types `book.time` as `number | undefined`, but the wire
384
+ // payload can return a string at runtime — we widen to unknown so the
385
+ // runtime guard against an empty string is meaningful (the SDK type
386
+ // would otherwise narrow it out).
387
+ const timeRaw = book.time;
388
+ if (timeRaw === "") {
389
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned empty string for time on ${coin}`, { exchange: "hyperliquid" });
390
+ }
391
+ const time = timeRaw === undefined || timeRaw === null ? 0 : Number(timeRaw);
392
+ if (!Number.isFinite(time)) {
393
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned non-finite time for ${coin}: ${timeRaw}`, { exchange: "hyperliquid" });
394
+ }
395
+ return {
396
+ outcome,
397
+ side,
398
+ time,
399
+ bids: levels[0].map((l) => [String(l.px ?? "0"), String(l.sz ?? "0")]),
400
+ asks: levels[1].map((l) => [String(l.px ?? "0"), String(l.sz ?? "0")]),
401
+ };
402
+ }
403
+ async placeOrder(opts) {
404
+ await this.init();
405
+ this._validateOutcomeSide(opts.outcome, opts.side);
406
+ const quote = this.quoteTokenOf(opts.outcome);
407
+ const notional = Number(opts.price) * Number(opts.size);
408
+ if (!Number.isFinite(notional) || notional < MIN_ORDER_QUOTE) {
409
+ throw new PerpError("INVALID_PARAMS", `Outcome order value must be at least ${MIN_ORDER_QUOTE} ${quote} (got price=${opts.price} * size=${opts.size} = ${notional.toFixed(4)})`, {
410
+ exchange: "hyperliquid",
411
+ remediation: `Increase price or size so price*size >= ${MIN_ORDER_QUOTE}`,
412
+ });
413
+ }
414
+ // Pre-check the quote-token spot balance (only on buy — sell consumes outcome shares).
415
+ if (opts.isBuy) {
416
+ const available = await this._getSpotAvailable(quote);
417
+ if (available < notional) {
418
+ throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient ${quote} for outcome buy: need ${notional.toFixed(4)} ${quote}, have ${available.toFixed(4)} (spot)`, {
419
+ exchange: "hyperliquid",
420
+ remediation: `Outcome ${opts.outcome} is quoted in ${quote}: hold ${quote} on Hyperliquid spot (perp-to-spot transfer or a spot swap)`,
421
+ });
422
+ }
423
+ }
424
+ const assetId = HyperliquidOutcomeAdapter.assetId(opts.outcome, opts.side);
425
+ const tif = opts.tif ?? "Gtc";
426
+ const action = {
427
+ type: "order",
428
+ orders: [{
429
+ a: assetId,
430
+ b: opts.isBuy,
431
+ p: this._trimZeros(opts.price),
432
+ s: this._trimZeros(opts.size),
433
+ r: false,
434
+ t: { limit: { tif } },
435
+ }],
436
+ grouping: "na",
437
+ };
438
+ const result = await this._hl.exchangeAction(action);
439
+ HyperliquidOutcomeAdapter._assertOrderStatusOk(result);
440
+ await this._invalidateAccountCache();
441
+ return result;
442
+ }
443
+ async cancelOrder(outcome, side, oid) {
444
+ await this.init();
445
+ this._validateOutcomeSide(outcome, side);
446
+ const assetId = HyperliquidOutcomeAdapter.assetId(outcome, side);
447
+ const result = await this._hl.exchangeAction({
448
+ type: "cancel",
449
+ cancels: [{ a: assetId, o: oid }],
450
+ });
451
+ HyperliquidOutcomeAdapter._assertCancelStatusOk(result);
452
+ await this._invalidateAccountCache();
453
+ return result;
454
+ }
455
+ /**
456
+ * Throw if the venue rejected the order embedded inside a top-level
457
+ * `status:"ok"` response (HL pattern). Returns silently for resting/filled.
458
+ */
459
+ static _assertOrderStatusOk(result) {
460
+ const r = result;
461
+ const statuses = r?.response?.data?.statuses;
462
+ if (!Array.isArray(statuses) || statuses.length === 0) {
463
+ throw new PerpError("EXCHANGE_ERROR", `Outcome order: empty status response (${JSON.stringify(result).slice(0, 200)})`, { exchange: "hyperliquid" });
464
+ }
465
+ const st = statuses[0];
466
+ if (st.error) {
467
+ // A per-entry error inside status "ok" is the venue's definite refusal of THIS order (nothing
468
+ // placed) — never a retryable EXCHANGE_ERROR. Live 2026-10-12: "Insufficient spot balance asset=100014881".
469
+ const msg = String(st.error);
470
+ const insufficient = /insufficient/i.test(msg);
471
+ throw new PerpError(insufficient ? "INSUFFICIENT_BALANCE" : "ORDER_REJECTED", `Outcome order rejected: ${msg}`, {
472
+ exchange: "hyperliquid", venueError: msg,
473
+ ...(insufficient ? { remediation: "Check the outcome's quote token balance on Hyperliquid spot: perp outcome list (quote per outcome)" } : {}),
474
+ });
475
+ }
476
+ }
477
+ static _assertCancelStatusOk(result) {
478
+ const r = result;
479
+ const statuses = r?.response?.data?.statuses;
480
+ if (!Array.isArray(statuses) || statuses.length === 0) {
481
+ throw new PerpError("EXCHANGE_ERROR", `Outcome cancel: empty status response`, { exchange: "hyperliquid" });
482
+ }
483
+ const st = statuses[0];
484
+ if (typeof st === "object" && st !== null && "error" in st) {
485
+ const msg = String(st.error);
486
+ throw new PerpError("ORDER_REJECTED", `Outcome cancel rejected: ${msg}`, { exchange: "hyperliquid", venueError: msg });
487
+ }
488
+ }
489
+ async getOpenOrders() {
490
+ await this.init();
491
+ const orders = await this._hl.getOpenOrders();
492
+ return orders.filter((o) => typeof o.symbol === "string" && o.symbol.startsWith("#"));
493
+ }
494
+ // ── Private ──────────────────────────────────────────────────────────────
495
+ /**
496
+ * The outcome's quote token from outcomeMeta (`quoteToken`, e.g. "USDC"). Unknown outcome →
497
+ * SYMBOL_NOT_FOUND; missing / non-string quoteToken → EXCHANGE_ERROR (never an assumed token).
498
+ */
499
+ quoteTokenOf(outcome) {
500
+ const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
501
+ if (!o) {
502
+ throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, { exchange: "hyperliquid", remediation: "Run: perp outcome list" });
503
+ }
504
+ if (typeof o.quoteToken !== "string" || o.quoteToken.trim() === "") {
505
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid outcomeMeta outcome ${outcome} has no quoteToken (got ${JSON.stringify(o.quoteToken ?? null)}) — its collateral token is unknown`, { exchange: "hyperliquid" });
506
+ }
507
+ return o.quoteToken;
508
+ }
509
+ /**
510
+ * The outcome / side behind an outcome coin ("#14881" = outcome 1488, side 1) with its names from
511
+ * outcomeMeta; null when the coin is not an outcome coin or the outcome is not listed.
512
+ */
513
+ describeCoin(coin) {
514
+ if (!/^[#+]\d+$/.test(coin))
515
+ return null;
516
+ const enc = Number(coin.slice(1));
517
+ const outcome = Math.floor(enc / 10);
518
+ const side = enc % 10;
519
+ const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
520
+ const sideName = o?.sideSpecs[side]?.name;
521
+ if (!o || sideName === undefined)
522
+ return null;
523
+ return { outcome, side, name: o.name, sideName };
524
+ }
525
+ /**
526
+ * Price of held outcome balance coins (`+<encoding>`, spotClearinghouseState) in the outcome's quote
527
+ * token: the side's mid `allMids["#<encoding>"]` (docs info-endpoint "Retrieve mids for all coins";
528
+ * live 2026-10-13 mainnet allMids "#14880": "0.100405", "#14881": "0.899595"). HIP-4 docs: a side
529
+ * token settles to `settleFraction` (Yes) / `1 - settleFraction` (No) quote tokens and the Yes/No
530
+ * books are merged (buy Yes at p = sell No at 1 - p), so the mid is the token's value in its quote.
531
+ * `quoteToken` null = the outcome / side is not in outcomeMeta (outcomeMeta is re-read once when a
532
+ * coin's outcome is missing — outcomes are deployed after init) or has no quoteToken; `mid` null = no
533
+ * positive mid. Coins that are not `+<digits>` are not returned. A malformed allMids → EXCHANGE_ERROR.
534
+ */
535
+ async getBalanceCoinPrices(coins) {
536
+ await this.init();
537
+ const held = coins.filter((c) => /^\+\d+$/.test(c)).map((coin) => {
538
+ const enc = Number(coin.slice(1));
539
+ return { coin, outcome: Math.floor(enc / 10), side: enc % 10, enc };
540
+ });
541
+ if (held.length === 0)
542
+ return [];
543
+ const find = (outcome) => this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
544
+ if (held.some((h) => find(h.outcome) === undefined))
545
+ await this._loadOutcomeMeta();
546
+ const allMids = await this._infoPost({ type: "allMids" });
547
+ if (allMids === null || typeof allMids !== "object" || Array.isArray(allMids)) {
548
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid allMids returned unexpected shape: ${JSON.stringify(allMids).slice(0, 200)}`, { exchange: "hyperliquid" });
549
+ }
550
+ const mids = allMids;
551
+ return held.map((h) => {
552
+ const o = find(h.outcome);
553
+ const quoteToken = o !== undefined && h.side < o.sideSpecs.length && typeof o.quoteToken === "string" && o.quoteToken.trim() !== ""
554
+ ? o.quoteToken : null;
555
+ const raw = mids[`#${h.enc}`];
556
+ const mid = typeof raw === "string" && Number(raw) > 0 ? raw : null;
557
+ return { coin: h.coin, quoteToken, mid };
558
+ });
559
+ }
560
+ _lookupSideName(outcome, side) {
561
+ const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
562
+ return o?.sideSpecs[side]?.name ?? `side${side}`;
563
+ }
564
+ _validateOutcomeSide(outcome, side) {
565
+ HyperliquidOutcomeAdapter._assertOutcomeRange(outcome, side);
566
+ const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
567
+ if (!o) {
568
+ throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, {
569
+ exchange: "hyperliquid",
570
+ remediation: "Run: perp outcome list",
571
+ });
572
+ }
573
+ if (side >= o.sideSpecs.length) {
574
+ throw new PerpError("INVALID_PARAMS", `Invalid side ${side} for outcome ${outcome} (valid: 0..${o.sideSpecs.length - 1})`, {
575
+ exchange: "hyperliquid",
576
+ remediation: "Run: perp outcome list",
577
+ });
578
+ }
579
+ }
580
+ /**
581
+ * Resolve the master EVM address for spot-clearinghouse queries.
582
+ * `HyperliquidAdapter._address` is populated only when a master/PK signer
583
+ * is configured. Agent-only setups (no master pk in OWS) leave it empty
584
+ * even though the agent meta knows the user's EVM address. Fall through
585
+ * to the agent registry rather than treating empty as zero balance —
586
+ * Rule #2: no silent fallback.
587
+ */
588
+ async _resolveUserAddress() {
589
+ if (this._hl.address)
590
+ return this._hl.address;
591
+ const { getAgent } = await import("../agent-wallet/store.js");
592
+ const agent = getAgent("hyperliquid");
593
+ if (agent?.userEvmAddress)
594
+ return agent.userEvmAddress;
595
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Hyperliquid user address not resolved — cannot query the spot balance state", { exchange: "hyperliquid", remediation: "Configure an OWS master wallet or register an HL agent" });
596
+ }
597
+ /**
598
+ * Available spot balance of `coin` (total − hold, spotClearinghouseState). A coin the account does
599
+ * not hold is not listed → 0 held. No balances list, or an unreadable total / hold → EXCHANGE_ERROR.
600
+ */
601
+ async _getSpotAvailable(coin) {
602
+ const userAddress = await this._resolveUserAddress();
603
+ const state = await this._infoPost({ type: "spotClearinghouseState", user: userAddress });
604
+ if (!Array.isArray(state?.balances)) {
605
+ throw new PerpError("EXCHANGE_ERROR", "Hyperliquid spotClearinghouseState has no balances list", { exchange: "hyperliquid" });
606
+ }
607
+ const row = state.balances.find((b) => String(b.coin) === coin);
608
+ if (!row)
609
+ return 0;
610
+ const total = Number(row.total);
611
+ const hold = Number(row.hold);
612
+ if (!Number.isFinite(total) || !Number.isFinite(hold)) {
613
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid spot ${coin} balance unreadable (total ${JSON.stringify(row.total)}, hold ${JSON.stringify(row.hold)})`, { exchange: "hyperliquid" });
614
+ }
615
+ return Math.max(0, total - hold);
616
+ }
617
+ async _invalidateAccountCache() {
618
+ try {
619
+ const { invalidateCache } = await import("../cache.js");
620
+ invalidateCache("acct");
621
+ }
622
+ catch {
623
+ // Cache module unavailable in test contexts; ignore.
624
+ }
625
+ }
626
+ _trimZeros(s) {
627
+ if (!s.includes("."))
628
+ return s;
629
+ const n = s.replace(/\.?0+$/, "");
630
+ return n === "" || n === "-0" ? "0" : n;
631
+ }
632
+ async _infoPost(body) {
633
+ const baseUrl = this._hl.isTestnet
634
+ ? "https://api.hyperliquid-testnet.xyz"
635
+ : "https://api.hyperliquid.xyz";
636
+ const res = await fetch(`${baseUrl}/info`, {
637
+ method: "POST",
638
+ headers: { "Content-Type": "application/json" },
639
+ body: JSON.stringify(body),
640
+ });
641
+ if (!res.ok) {
642
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid /info HTTP ${res.status} for ${JSON.stringify(body)}`, { exchange: "hyperliquid", status: res.status });
643
+ }
644
+ return res.json();
645
+ }
646
+ }