perp-cli 0.12.15 → 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 +278 -31
  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 +74 -84
  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 +157 -682
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +14 -0
  287. package/dist/landing.js +36 -0
  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 +338 -134
  348. package/skills/perp-cli/references/agent-operations.md +181 -92
  349. package/skills/perp-cli/references/commands.md +475 -132
  350. package/skills/perp-cli/references/strategies.md +44 -38
  351. package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
  352. package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
  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 +33 -16
  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
@@ -6,9 +6,10 @@
6
6
  *
7
7
  * Composes with HyperliquidAdapter for signing and API access.
8
8
  */
9
- import type { SpotAdapter, SpotMarketInfo, SpotBalance } from "./spot-interface.js";
9
+ import type { SpotAdapter, SpotMarketInfo, SpotBalance, SpotHoldingPrice, SpotHoldingPricesCapable } from "./spot-interface.js";
10
+ import type { ResolvedSlippage } from "./slippage.js";
10
11
  import type { HyperliquidAdapter } from "./hyperliquid.js";
11
- export declare class HyperliquidSpotAdapter implements SpotAdapter {
12
+ export declare class HyperliquidSpotAdapter implements SpotAdapter, SpotHoldingPricesCapable {
12
13
  readonly name = "hyperliquid";
13
14
  private _hl;
14
15
  private _spotAssetMap;
@@ -16,29 +17,67 @@ export declare class HyperliquidSpotAdapter implements SpotAdapter {
16
17
  private _spotDecimals;
17
18
  private _spotCanonical;
18
19
  private _initialized;
20
+ private _outcome;
19
21
  private _cachedMetaCtx;
20
22
  constructor(hlAdapter: HyperliquidAdapter);
21
23
  /** Load spot metadata from the Hyperliquid spot API */
22
24
  init(): Promise<void>;
23
25
  private _loadSpotMeta;
24
26
  getSpotMarkets(): Promise<SpotMarketInfo[]>;
27
+ /**
28
+ * Prices of held HIP-4 outcome tokens (`+<encoding>` balance rows — not spot markets) for valuation:
29
+ * the outcome side's mid in the outcome's quote token (HyperliquidOutcomeAdapter.getBalanceCoinPrices).
30
+ * Other tokens are not returned (they stay unpriced).
31
+ */
32
+ getHoldingPrices(tokens: readonly string[]): Promise<SpotHoldingPrice[]>;
25
33
  getSpotOrderbook(symbol: string): Promise<{
26
34
  bids: [string, string][];
27
35
  asks: [string, string][];
28
36
  }>;
29
37
  getSpotBalances(): Promise<SpotBalance[]>;
30
- spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
38
+ /** Uncached read (bypasses the shared acct: TTL cache) — fill measurement / verification. */
39
+ getSpotBalancesFresh(): Promise<SpotBalance[]>;
40
+ private _spotBalances;
41
+ spotMarketOrder(symbol: string, side: "buy" | "sell", size: string, opts: {
42
+ slippage: ResolvedSlippage;
43
+ }): Promise<unknown>;
44
+ /** Definite IOC outcome of a raw spot order response (same contract as HyperliquidAdapter.iocOutcomeOf). */
45
+ iocOutcomeOf(raw: unknown): {
46
+ filledSize: string;
47
+ final: true;
48
+ } | null;
31
49
  spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
32
50
  tif?: string;
33
51
  }): Promise<unknown>;
52
+ /**
53
+ * Open spot orders (info `openOrders`, spot coins only: "@<spotIndex>" or "PURR/USDC").
54
+ * Symbols are "BASE/QUOTE" from spot meta; a coin missing from meta → EXCHANGE_ERROR (never guessed).
55
+ */
56
+ getSpotOpenOrders(symbol: string | null): Promise<Array<{
57
+ orderId: string;
58
+ symbol: string;
59
+ side: "buy" | "sell";
60
+ price: string;
61
+ size: string;
62
+ filled: string;
63
+ }>>;
34
64
  spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
35
65
  /**
36
66
  * Transfer USDC between perp and spot accounts.
37
- * HL uses separate balances — spot buys require USDC in the spot account.
38
- * Uses SDK's transferBetweenSpotAndPerp (userSignedAction, not L1 action).
67
+ * HL uses separate balances (standard mode) — spot buys require USDC in the spot account.
39
68
  */
40
69
  transferUsdcToSpot(amount: number): Promise<unknown>;
41
70
  transferUsdcToPerp(amount: number): Promise<unknown>;
71
+ /**
72
+ * Spot↔perp USDC move by the active signer tier: an agent signs the L1
73
+ * `agentSendAsset` (agents cannot sign user-signed actions); the master signs
74
+ * `usdClassTransfer`. No signer → NO_SIGNER_AVAILABLE from the adapter.
75
+ */
76
+ private _moveUsdc;
77
+ /** SpotTransferCapable (§12.4 #4): USDC only; an `err` response is thrown, not returned. */
78
+ transferToSpot(amount: string, asset: string): Promise<unknown>;
79
+ transferToPerp(amount: string, asset: string): Promise<unknown>;
80
+ private _checkedTransfer;
42
81
  /** Get the spot asset index (without the 10000 offset) */
43
82
  getSpotIndex(base: string): number | undefined;
44
83
  /** Get spot size decimals for a base token — from API data, not hardcoded */
@@ -6,19 +6,15 @@
6
6
  *
7
7
  * Composes with HyperliquidAdapter for signing and API access.
8
8
  */
9
+ import { HyperliquidOutcomeAdapter } from "./hyperliquid-outcome.js";
9
10
  import { PERP_TO_SPOT_MAP, SPOT_PERP_TOKEN_MAP } from "./spot-interface.js";
10
- /** HL official price rounding: 5 significant figures, max 8 decimals for spot */
11
- function hlRoundPrice(px, szDecimals) {
12
- const MAX_DECIMALS = 8; // spot uses 8 (perp uses 6)
13
- if (px > 100_000)
14
- return String(Math.round(px));
15
- const sig5 = Number(px.toPrecision(5));
16
- const priceDec = Math.max(0, MAX_DECIMALS - szDecimals);
17
- return Number(sig5.toFixed(priceDec)).toString();
18
- }
19
- /** HL official size rounding */
20
- function hlRoundSize(sz, szDecimals) {
21
- return sz.toFixed(szDecimals);
11
+ import { assertHlAppliedBps, hlIocOutcome, hlProtectionPrice, hlRequirePriceOnGrid, hlSizeStep, hlTif } from "./hyperliquid.js";
12
+ import { floorSize } from "./precision.js";
13
+ import { PerpError, isDefiniteOrderFailure, isTransportFailure, orderNotSent, orderOutcomeUnknown } from "../errors.js";
14
+ import { invalidateCache } from "../cache.js";
15
+ /** HL size: floored to the szDecimals lot (never rounded up past the request); 0 → SIZE_TOO_SMALL. */
16
+ function hlRoundSize(size, szDecimals) {
17
+ return floorSize(size, hlSizeStep(szDecimals));
22
18
  }
23
19
  export class HyperliquidSpotAdapter {
24
20
  name = "hyperliquid";
@@ -28,6 +24,7 @@ export class HyperliquidSpotAdapter {
28
24
  _spotDecimals = new Map();
29
25
  _spotCanonical = new Map(); // "PURR" → true, "UBTC" → false
30
26
  _initialized = false;
27
+ _outcome = null;
31
28
  _cachedMetaCtx = null;
32
29
  constructor(hlAdapter) {
33
30
  this._hl = hlAdapter;
@@ -72,10 +69,11 @@ export class HyperliquidSpotAdapter {
72
69
  continue;
73
70
  this._spotAssetMap.set(key, u.index);
74
71
  this._spotAssetReverse.set(u.index, key);
75
- this._spotDecimals.set(key, {
76
- size: tokenSzDecimals.get(baseTokenIndex) ?? 2,
77
- price: 6,
78
- });
72
+ // Rule #2: no assumed szDecimals — a token without one has no decimals entry and
73
+ // its orders are refused (SYMBOL_NOT_FOUND) instead of being sized on a guess.
74
+ const szDecimals = tokenSzDecimals.get(baseTokenIndex);
75
+ if (szDecimals !== undefined)
76
+ this._spotDecimals.set(key, { size: szDecimals, price: Math.max(0, 8 - szDecimals) }); // HL spot price grid: max 8 − szDecimals decimals
79
77
  this._spotCanonical.set(key, u.isCanonical === true);
80
78
  }
81
79
  }
@@ -85,48 +83,67 @@ export class HyperliquidSpotAdapter {
85
83
  }
86
84
  async getSpotMarkets() {
87
85
  await this.init();
88
- try {
89
- // Reuse cached metaCtx from init, or fetch fresh if cache expired
90
- const metaCtx = this._cachedMetaCtx
91
- ?? await this._infoPost({ type: "spotMetaAndAssetCtxs" });
92
- if (!metaCtx?.[0]?.universe)
93
- return [];
94
- const tokenNames = new Map();
95
- const tokenSzDec = new Map();
96
- for (const t of metaCtx[0].tokens ?? []) {
97
- tokenNames.set(t.index, t.name);
98
- if (t.szDecimals !== undefined)
99
- tokenSzDec.set(t.index, t.szDecimals);
100
- }
101
- // Fetch accurate mid prices from allMids (markPx in metaCtx is unreliable for spot)
102
- const allMids = await this._infoPost({ type: "allMids" });
103
- const usdcTokenIndex = metaCtx[0].tokens?.find(t => t.name === "USDC")?.index ?? 0;
104
- const markets = [];
105
- for (const u of metaCtx[0].universe) {
106
- // Only USDC-quoted pairs
107
- if (u.tokens[1] !== usdcTokenIndex)
108
- continue;
109
- const baseToken = tokenNames.get(u.tokens[0]) ?? "";
110
- if (!baseToken)
111
- continue;
112
- // Price from allMids using universe.name (e.g., "PURR/USDC" or "@107")
113
- const midPrice = allMids[u.name] ?? "0";
114
- const spotDec = this._spotDecimals.get(baseToken.toUpperCase());
115
- markets.push({
116
- symbol: `${baseToken}/USDC`,
117
- baseToken,
118
- quoteToken: "USDC",
119
- markPrice: midPrice,
120
- volume24h: "0",
121
- sizeDecimals: spotDec?.size ?? tokenSzDec.get(u.tokens[0]) ?? 2,
122
- priceDecimals: spotDec?.price ?? 6,
123
- });
124
- }
125
- return markets;
126
- }
127
- catch {
86
+ // Rule #2: no catch→[]. A failed spotMetaAndAssetCtxs/allMids fetch must throw,
87
+ // not be coerced to an empty markets list (which reads as "this venue has no spot
88
+ // markets" and silently disables spot-perp arb discovery/sizing).
89
+ // Reuse cached metaCtx from init, or fetch fresh if cache expired.
90
+ const metaCtx = this._cachedMetaCtx
91
+ ?? await this._infoPost({ type: "spotMetaAndAssetCtxs" });
92
+ if (!metaCtx?.[0]?.universe)
128
93
  return [];
94
+ const tokenNames = new Map();
95
+ const tokenSzDec = new Map();
96
+ for (const t of metaCtx[0].tokens ?? []) {
97
+ tokenNames.set(t.index, t.name);
98
+ if (t.szDecimals !== undefined)
99
+ tokenSzDec.set(t.index, t.szDecimals);
100
+ }
101
+ // Fetch accurate mid prices from allMids (markPx in metaCtx is unreliable for spot)
102
+ const allMids = await this._infoPost({ type: "allMids" });
103
+ const usdcTokenIndex = metaCtx[0].tokens?.find(t => t.name === "USDC")?.index ?? 0;
104
+ const markets = [];
105
+ for (const [ui, u] of metaCtx[0].universe.entries()) {
106
+ // Only USDC-quoted pairs
107
+ if (u.tokens[1] !== usdcTokenIndex)
108
+ continue;
109
+ const baseToken = tokenNames.get(u.tokens[0]) ?? "";
110
+ if (!baseToken)
111
+ continue;
112
+ // Price from allMids using universe.name (e.g., "PURR/USDC" or "@107")
113
+ const midPrice = allMids[u.name] ?? "0";
114
+ const spotDec = this._spotDecimals.get(baseToken.toUpperCase());
115
+ const sizeDecimals = spotDec?.size ?? tokenSzDec.get(u.tokens[0]);
116
+ // Rule #2: a market without a published szDecimals is not listed (never sized on a guess).
117
+ if (sizeDecimals === undefined)
118
+ continue;
119
+ markets.push({
120
+ symbol: `${baseToken}/USDC`,
121
+ baseToken,
122
+ quoteToken: "USDC",
123
+ markPrice: midPrice,
124
+ // asset ctxs are index-aligned with universe; dayNtlVlm = 24h quote volume
125
+ volume24h: metaCtx[1]?.[ui]?.dayNtlVlm !== undefined ? String(metaCtx[1][ui].dayNtlVlm) : null,
126
+ sizeDecimals,
127
+ // HL spot price grid: at most 8 − szDecimals decimals (venue rule) — derived, never a fixed guess.
128
+ priceDecimals: spotDec !== undefined ? spotDec.price : Math.max(0, 8 - sizeDecimals),
129
+ });
129
130
  }
131
+ return markets;
132
+ }
133
+ /**
134
+ * Prices of held HIP-4 outcome tokens (`+<encoding>` balance rows — not spot markets) for valuation:
135
+ * the outcome side's mid in the outcome's quote token (HyperliquidOutcomeAdapter.getBalanceCoinPrices).
136
+ * Other tokens are not returned (they stay unpriced).
137
+ */
138
+ async getHoldingPrices(tokens) {
139
+ const coins = tokens.filter(t => /^\+\d+$/.test(t));
140
+ if (coins.length === 0)
141
+ return [];
142
+ if (this._outcome === null)
143
+ this._outcome = new HyperliquidOutcomeAdapter(this._hl);
144
+ return (await this._outcome.getBalanceCoinPrices(coins)).map(p => ({
145
+ token: p.coin, quoteToken: p.quoteToken, markPrice: p.quoteToken === null ? null : p.mid,
146
+ }));
130
147
  }
131
148
  async getSpotOrderbook(symbol) {
132
149
  await this.init();
@@ -151,86 +168,245 @@ export class HyperliquidSpotAdapter {
151
168
  };
152
169
  }
153
170
  async getSpotBalances() {
171
+ return this._spotBalances(false);
172
+ }
173
+ /** Uncached read (bypasses the shared acct: TTL cache) — fill measurement / verification. */
174
+ async getSpotBalancesFresh() {
175
+ return this._spotBalances(true);
176
+ }
177
+ async _spotBalances(fresh) {
178
+ // Rule #2: no catch→[]. A failed spotClearinghouseState fetch must surface
179
+ // honestly — an empty list is indistinguishable from a genuinely empty wallet
180
+ // and silently drives wrong USDC-transfer sizing / false post-fill verification
181
+ // in the funding-arb bot (and a misleading empty holdings table).
182
+ // Reuse cached spot clearinghouse state (shared with getBalance() — saves 1 API call).
183
+ const state = await this._hl._getSpotClearinghouseState({ fresh });
184
+ const balances = (state?.balances ?? []);
185
+ // Unified HL accounts: spot USDC IS the perp margin (§12.4 #8) — mark it shared.
186
+ // HIP-3 dex-scoped adapters report dex equity, which never contains spot USDC.
187
+ const unified = this._hl.isUnifiedAccount === true && this._hl.dex === "";
188
+ return balances.map((b) => ({
189
+ token: String(b.coin ?? ""),
190
+ total: String(b.total ?? "0"),
191
+ available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
192
+ held: String(b.hold ?? "0"),
193
+ entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
194
+ shared: unified && String(b.coin ?? "") === "USDC",
195
+ }));
196
+ }
197
+ async spotMarketOrder(symbol, side, size, opts) {
198
+ const what = `spot ${side} ${size} ${symbol}`;
199
+ // Pre-send (resolution, reference price): a failure is a definite "not placed".
200
+ let base;
201
+ let order;
202
+ let protection;
154
203
  try {
155
- // Reuse cached spot clearinghouse state (shared with getBalance() — saves 1 API call)
156
- const state = await this._hl._getSpotClearinghouseState();
157
- const balances = (state?.balances ?? []);
158
- return balances.map((b) => ({
159
- token: String(b.coin ?? ""),
160
- total: String(b.total ?? "0"),
161
- available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
162
- held: String(b.hold ?? "0"),
163
- entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
164
- }));
204
+ await this.init();
205
+ const resolved = this._resolveBase(symbol);
206
+ base = resolved.base;
207
+ // Reference = l2Book mid (the same single declared source as HL perps, §13.10 #5).
208
+ // Either side missing → PRICE_STALE (no one-sided "mid").
209
+ const book = await this.getSpotOrderbook(symbol);
210
+ const bestAsk = Number(book.asks[0]?.[0]);
211
+ const bestBid = Number(book.bids[0]?.[0]);
212
+ if (!(bestAsk > 0) || !(bestBid > 0) || !Number.isFinite(bestAsk) || !Number.isFinite(bestBid)) {
213
+ throw new PerpError("PRICE_STALE", `Hyperliquid spot ${base}: no two-sided l2Book to derive the mid reference price`, { exchange: "hyperliquid" });
214
+ }
215
+ const midPrice = (bestAsk + bestBid) / 2;
216
+ // Protection = mid ± the resolved slippage bound (design §13.2), aligned toward
217
+ // crossing on HL's spot grid (5 sig figs, max 8 − szDecimals decimals).
218
+ const isBuy = side === "buy";
219
+ const szDec = this.getSpotDecimals(base).size;
220
+ const { protectionPrice, appliedBps } = hlProtectionPrice(midPrice, side, opts.slippage.bps, szDec, 8);
221
+ await assertHlAppliedBps(appliedBps, `spot ${base} protection ${protectionPrice} vs reference ${midPrice}`);
222
+ protection = { referencePrice: String(midPrice), protectionPrice, appliedBps };
223
+ order = { assetIndex: 10000 + resolved.spotIndex, isBuy, price: protectionPrice, size: hlRoundSize(size, szDec) };
165
224
  }
166
- catch {
167
- return [];
225
+ catch (e) {
226
+ throw orderNotSent("hyperliquid", what, e);
227
+ }
228
+ // Send. A transport failure / unreadable body after the request may have placed
229
+ // the order → ORDER_PENDING. Any other error is the venue's explicit
230
+ // {"status":"err"} rejection or a signing failure before the request → definite.
231
+ let result;
232
+ try {
233
+ result = await this._rawPlaceSpotOrder({ ...order, orderType: { limit: { tif: "Ioc" } } });
168
234
  }
235
+ catch (e) {
236
+ if (isTransportFailure(e) || (e instanceof Error && e.message.startsWith("HL exchange API error ("))) {
237
+ throw orderOutcomeUnknown("hyperliquid", what, e);
238
+ }
239
+ if (e instanceof PerpError)
240
+ throw isDefiniteOrderFailure(e) ? e : orderOutcomeUnknown("hyperliquid", what, e);
241
+ throw new PerpError("ORDER_REJECTED", `hyperliquid ${what} rejected: ${e instanceof Error ? e.message : String(e)}`, { exchange: "hyperliquid" });
242
+ }
243
+ // Validate fill — HL returns status "ok" even for 0-fill IoC orders
244
+ this._validateOrderFill(result, base, side, order.size);
245
+ // The protection sent rides along (`protection`, read by the CLI's uniform spot output);
246
+ // `sentSize` = the size actually sent (floored to szDecimals).
247
+ return { ...result, protection, sentSize: order.size };
169
248
  }
170
- async spotMarketOrder(symbol, side, size) {
249
+ /** Definite IOC outcome of a raw spot order response (same contract as HyperliquidAdapter.iocOutcomeOf). */
250
+ iocOutcomeOf(raw) {
251
+ return hlIocOutcome(raw);
252
+ }
253
+ async spotLimitOrder(symbol, side, price, size, opts) {
171
254
  await this.init();
172
255
  const { base, spotIndex } = this._resolveBase(symbol);
173
256
  const assetIndex = 10000 + spotIndex;
174
- // Get mid price for slippage calculation
175
- const book = await this.getSpotOrderbook(symbol);
176
- const bestAsk = book.asks.length > 0 ? Number(book.asks[0][0]) : 0;
177
- const bestBid = book.bids.length > 0 ? Number(book.bids[0][0]) : 0;
178
- const midPrice = (bestAsk + bestBid) / 2;
179
- if (midPrice <= 0)
180
- throw new Error(`Cannot get price for spot ${base}`);
181
- // 5% slippage for market orders
182
- const slippage = 0.05;
183
- const isBuy = side === "buy";
184
- const slippagePrice = isBuy ? midPrice * (1 + slippage) : midPrice * (1 - slippage);
185
- const dec = this._spotDecimals.get(base);
186
- const szDec = dec?.size ?? 2;
187
- const result = await this._rawPlaceSpotOrder({
188
- assetIndex,
189
- isBuy,
190
- price: hlRoundPrice(slippagePrice, szDec),
191
- size: hlRoundSize(Number(size), szDec),
192
- orderType: { limit: { tif: "Ioc" } },
193
- });
194
- // Validate fill — HL returns status "ok" even for 0-fill IoC orders
195
- this._validateOrderFill(result, base, side, size);
257
+ // TIF aliases (§13.3): GTC/IOC/ALO or native spellings; FOK → CAPABILITY_UNSUPPORTED.
258
+ const tif = hlTif(opts?.tif ?? "GTC");
259
+ const szDec = this.getSpotDecimals(base).size;
260
+ const what = `spot limit ${side} ${size} ${base} @ ${price}`;
261
+ // Pre-send: off-grid price → INVALID_TICK (refused, never rounded); size floored to the lot.
262
+ const sentPrice = hlRequirePriceOnGrid(price, szDec, 8, "spot limit price");
263
+ const sentSize = hlRoundSize(size, szDec);
264
+ // Same send contract as spotMarketOrder: a transport failure after the request may
265
+ // have placed the order → ORDER_PENDING; a {"status":"err"} body → definite rejection.
266
+ let result;
267
+ try {
268
+ result = await this._rawPlaceSpotOrder({
269
+ assetIndex,
270
+ isBuy: side === "buy",
271
+ price: sentPrice,
272
+ size: sentSize,
273
+ orderType: { limit: { tif } },
274
+ });
275
+ }
276
+ catch (e) {
277
+ if (isTransportFailure(e) || (e instanceof Error && e.message.startsWith("HL exchange API error ("))) {
278
+ throw orderOutcomeUnknown("hyperliquid", what, e);
279
+ }
280
+ if (e instanceof PerpError)
281
+ throw isDefiniteOrderFailure(e) ? e : orderOutcomeUnknown("hyperliquid", what, e);
282
+ throw new PerpError("ORDER_REJECTED", `hyperliquid ${what} rejected: ${e instanceof Error ? e.message : String(e)}`, { exchange: "hyperliquid" });
283
+ }
284
+ // Venue-final like perps: statuses[0] = resting {oid} (open) | filled {totalSz, avgPx, oid}
285
+ // | error (e.g. "Insufficient spot balance asset=10000") → ORDER_REJECTED, nothing placed.
286
+ // A missing status after an accepted request → EXCHANGE_ERROR (HyperliquidAdapter._validateVenueAck).
287
+ this._hl._validateVenueAck(result, what);
196
288
  return result;
197
289
  }
198
- async spotLimitOrder(symbol, side, price, size, opts) {
290
+ /**
291
+ * Open spot orders (info `openOrders`, spot coins only: "@<spotIndex>" or "PURR/USDC").
292
+ * Symbols are "BASE/QUOTE" from spot meta; a coin missing from meta → EXCHANGE_ERROR (never guessed).
293
+ */
294
+ async getSpotOpenOrders(symbol) {
199
295
  await this.init();
200
- const { base, spotIndex } = this._resolveBase(symbol);
201
- const assetIndex = 10000 + spotIndex;
202
- const rawTif = opts?.tif ?? "Gtc";
203
- const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
204
- const tif = tifMap[rawTif.toUpperCase()] ?? rawTif;
205
- const dec = this._spotDecimals.get(base);
206
- const szDec = dec?.size ?? 2;
207
- return this._rawPlaceSpotOrder({
208
- assetIndex,
209
- isBuy: side === "buy",
210
- price: hlRoundPrice(Number(price), szDec),
211
- size: hlRoundSize(Number(size), szDec),
212
- orderType: { limit: { tif } },
213
- });
296
+ const meta = this._cachedMetaCtx?.[0];
297
+ if (meta === undefined)
298
+ throw new PerpError("EXCHANGE_ERROR", "Hyperliquid spot meta not loaded", { exchange: "hyperliquid" });
299
+ const tokenName = new Map(meta.tokens.map(t => [t.index, t.name]));
300
+ const pairName = (spotIndex) => {
301
+ const u = meta.universe.find(x => x.index === spotIndex);
302
+ const b = u === undefined ? undefined : tokenName.get(u.tokens[0]);
303
+ const q = u === undefined ? undefined : tokenName.get(u.tokens[1]);
304
+ if (b === undefined || q === undefined)
305
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid spot open order on unknown spot index ${spotIndex}`, { exchange: "hyperliquid" });
306
+ return `${b.toUpperCase()}/${q.toUpperCase()}`;
307
+ };
308
+ const rows = await this._infoPost({ type: "openOrders", user: this._hl.address });
309
+ if (!Array.isArray(rows))
310
+ throw new PerpError("EXCHANGE_ERROR", "Hyperliquid openOrders did not return an array", { exchange: "hyperliquid" });
311
+ const wantBase = symbol === null ? null : symbol.toUpperCase().split("/")[0];
312
+ const out = [];
313
+ for (const r of rows) {
314
+ const coin = String(r.coin ?? "");
315
+ let pair;
316
+ if (/^@\d+$/.test(coin))
317
+ pair = pairName(Number(coin.slice(1)));
318
+ else if (coin.includes("/"))
319
+ pair = coin.toUpperCase();
320
+ else
321
+ continue; // perp order
322
+ if (wantBase !== null && pair.split("/")[0] !== wantBase)
323
+ continue;
324
+ const orig = String(r.origSz ?? "");
325
+ const left = String(r.sz ?? "");
326
+ if (!/^\d+(\.\d+)?$/.test(orig) || !/^\d+(\.\d+)?$/.test(left)) {
327
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid open order ${String(r.oid)}: unexpected size fields`, { exchange: "hyperliquid" });
328
+ }
329
+ const dec = Math.max((orig.split(".")[1] ?? "").length, (left.split(".")[1] ?? "").length);
330
+ out.push({
331
+ orderId: String(r.oid),
332
+ symbol: pair,
333
+ side: r.side === "B" ? "buy" : "sell",
334
+ price: String(r.limitPx),
335
+ size: orig,
336
+ filled: (Number(orig) - Number(left)).toFixed(dec),
337
+ });
338
+ }
339
+ return out;
214
340
  }
215
341
  async spotCancelOrder(symbol, orderId) {
216
342
  await this.init();
217
343
  const { spotIndex } = this._resolveBase(symbol);
218
344
  const assetIndex = 10000 + spotIndex;
345
+ const oid = Number(orderId);
346
+ if (!/^\d+$/.test(orderId) || !Number.isSafeInteger(oid)) {
347
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid order id must be a decimal integer (got "${orderId}"); nothing was cancelled`, {
348
+ exchange: "hyperliquid", remediation: `Look up the order id: perp -e hyperliquid trade spot-orders ${symbol}`,
349
+ });
350
+ }
351
+ // statuses[0] "success" → cancelled; {error} → ORDER_REJECTED (trade.ts spotCancelResult).
219
352
  return this._signAndSendAction({
220
353
  type: "cancel",
221
- cancels: [{ a: assetIndex, o: parseInt(orderId) }],
354
+ cancels: [{ a: assetIndex, o: oid }],
222
355
  });
223
356
  }
224
357
  /**
225
358
  * Transfer USDC between perp and spot accounts.
226
- * HL uses separate balances — spot buys require USDC in the spot account.
227
- * Uses SDK's transferBetweenSpotAndPerp (userSignedAction, not L1 action).
359
+ * HL uses separate balances (standard mode) — spot buys require USDC in the spot account.
228
360
  */
229
361
  async transferUsdcToSpot(amount) {
230
- return this._hl.client.exchange.transferBetweenSpotAndPerp(amount, false);
362
+ return this._moveUsdc(String(amount), false);
231
363
  }
232
364
  async transferUsdcToPerp(amount) {
233
- return this._hl.client.exchange.transferBetweenSpotAndPerp(amount, true);
365
+ return this._moveUsdc(String(amount), true);
366
+ }
367
+ /**
368
+ * Spot↔perp USDC move by the active signer tier: an agent signs the L1
369
+ * `agentSendAsset` (agents cannot sign user-signed actions); the master signs
370
+ * `usdClassTransfer`. No signer → NO_SIGNER_AVAILABLE from the adapter.
371
+ */
372
+ async _moveUsdc(amount, toPerp) {
373
+ // The master's usdClassTransfer moves USDC between spot and the NATIVE perp dex only. On a
374
+ // HIP-3 dex adapter it would fund the wrong dex, so it is refused (the agent's
375
+ // agentSendAsset names the dex explicitly).
376
+ if (this._hl.dex !== "" && this._hl.activeSignerTier !== "agent") {
377
+ throw new PerpError("NOT_IMPLEMENTED", `Hyperliquid spot↔perp transfer for HIP-3 dex "${this._hl.dex}" is not implemented for the master wallet (usdClassTransfer only funds the native perp dex)`, {
378
+ exchange: "hyperliquid",
379
+ remediation: `Use an agent wallet (perp wallet agent approve hyperliquid), or move the collateral to dex "${this._hl.dex}" in the Hyperliquid app`,
380
+ });
381
+ }
382
+ try {
383
+ return await (this._hl.activeSignerTier === "agent"
384
+ ? this._hl.agentSendAsset(amount, toPerp)
385
+ : this._hl.usdClassTransfer(amount, toPerp));
386
+ }
387
+ finally {
388
+ invalidateCache("acct"); // perp and spot balances both moved
389
+ }
390
+ }
391
+ /** SpotTransferCapable (§12.4 #4): USDC only; an `err` response is thrown, not returned. */
392
+ async transferToSpot(amount, asset) {
393
+ return this._checkedTransfer(amount, asset, false);
394
+ }
395
+ async transferToPerp(amount, asset) {
396
+ return this._checkedTransfer(amount, asset, true);
397
+ }
398
+ async _checkedTransfer(amount, asset, toPerp) {
399
+ if (asset !== "USDC") {
400
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid spot↔perp transfer only moves USDC (got ${asset})`, { exchange: "hyperliquid" });
401
+ }
402
+ const n = Number(amount);
403
+ if (!Number.isFinite(n) || n <= 0)
404
+ throw new PerpError("INVALID_PARAMS", `Transfer amount must be positive (got ${amount})`);
405
+ const res = await this._moveUsdc(amount, toPerp);
406
+ if (res?.status === "err") {
407
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid ${toPerp ? "spot→perp" : "perp→spot"} transfer rejected: ${String(res.response)}`, { exchange: "hyperliquid" });
408
+ }
409
+ return res;
234
410
  }
235
411
  /** Get the spot asset index (without the 10000 offset) */
236
412
  getSpotIndex(base) {
@@ -241,7 +417,10 @@ export class HyperliquidSpotAdapter {
241
417
  const dec = this._spotDecimals.get(base.toUpperCase());
242
418
  if (dec)
243
419
  return dec;
244
- throw new Error(`No decimal info for spot token ${base}. Ensure init() completed successfully.`);
420
+ throw new PerpError("SYMBOL_NOT_FOUND", `Hyperliquid spot ${base.toUpperCase()}: no szDecimals in spot meta — refusing to size the order on a guess`, {
421
+ exchange: "hyperliquid",
422
+ remediation: "Retry (spot meta may not have loaded); if it persists the token is not tradeable via this CLI",
423
+ });
245
424
  }
246
425
  /**
247
426
  * Resolve a symbol to its actual spot base token and index.
@@ -282,16 +461,21 @@ export class HyperliquidSpotAdapter {
282
461
  const r = result;
283
462
  const statuses = r?.response?.data?.statuses;
284
463
  if (!statuses || statuses.length === 0) {
285
- throw new Error(`Spot ${side} ${base}: no order status in response`);
464
+ // Accepted request without a per-order status: the outcome is unknown.
465
+ throw orderOutcomeUnknown("hyperliquid", `spot ${side} ${base}`, new Error("no order status in response"));
286
466
  }
287
467
  const st = statuses[0];
288
468
  if (st.error) {
289
- throw new Error(`Spot ${side} ${base}: ${st.error}`);
469
+ throw new PerpError("ORDER_REJECTED", `Spot ${side} ${base}: ${st.error}`, { exchange: "hyperliquid" });
290
470
  }
291
471
  const filled = st.filled;
472
+ if (st.resting !== undefined) {
473
+ // An IoC never rests; a resting status means the order is live — do not resubmit.
474
+ throw orderOutcomeUnknown("hyperliquid", `spot ${side} ${base}`, new Error("IoC order reported resting"));
475
+ }
292
476
  if (!filled || Number(filled.totalSz ?? 0) === 0) {
293
- // Resting or 0-fill → for IoC market orders this means no fill
294
- throw new Error(`Spot ${side} ${base}: order not filled (0 of ${requestedSize} ${base}). Check price/liquidity.`);
477
+ // 0-fill IoC: cancelled by the venue, nothing filled (definite).
478
+ throw new PerpError("ORDER_REJECTED", `Spot ${side} ${base}: order not filled (0 of ${requestedSize} ${base}). Check price/liquidity.`, { exchange: "hyperliquid" });
295
479
  }
296
480
  }
297
481
  async _rawPlaceSpotOrder(opts) {
@@ -317,7 +501,14 @@ export class HyperliquidSpotAdapter {
317
501
  return this._signAndSendAction(action);
318
502
  }
319
503
  async _signAndSendAction(action) {
320
- return this._hl.exchangeAction(action);
504
+ try {
505
+ return await this._hl.exchangeAction(action);
506
+ }
507
+ finally {
508
+ // Spot balances / open orders changed (or may have): later reads in this or another
509
+ // process (the acct: disk cache is shared) must not serve the pre-order snapshot.
510
+ invalidateCache("acct");
511
+ }
321
512
  }
322
513
  async _infoPost(body) {
323
514
  const baseUrl = this._hl.isTestnet