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
@@ -0,0 +1,940 @@
1
+ /**
2
+ * TradingV2 for the four pre-existing venues (pacifica, hyperliquid, lighter /
3
+ * lighter-rh, aster) — design §13.7, §13.10 #8.
4
+ *
5
+ * Composition, not modification: the legacy ExchangeAdapter methods keep returning
6
+ * their raw venue responses (in-process contract for smart-order, perp-order,
7
+ * plan-executor, grid). This wrapper calls the same adapter methods (same send
8
+ * path) and normalizes the raw response into an OrderResult + execution record.
9
+ * The raw response is kept verbatim in `venueRaw`.
10
+ *
11
+ * Status rules (SSOT Rule #2): a status / fill is reported only when the venue
12
+ * response proves it; otherwise "accepted" with finality "pending".
13
+ */
14
+ import { PerpError, orderOutcomeUnknown } from "../errors.js";
15
+ import { isPacificaStopType, pacificaRefused } from "./pacifica.js";
16
+ import { iocLotTolerance, isIocOutcomeCapable } from "./ioc-outcome.js";
17
+ import { isQueryOrderCapable } from "./capabilities.js";
18
+ import { floorSize, lotStepOf } from "./precision.js";
19
+ import { getVenue, requireCapabilities } from "./venue.js";
20
+ import { isTradingV2, unsupported, validateOrderRequest } from "./trading-v2.js";
21
+ import { LIGHTER_GTT_EXPIRY_MS } from "./venues/lighter-family.js";
22
+ import { builderDecisionOf, builderGateOf, builderRejectionOf, NO_BUILDER } from "./builder-fee.js";
23
+ import { withExecution, } from "./venue-types.js";
24
+ function isLegacyFamily(f) {
25
+ return f === "pacifica" || f === "hyperliquid" || f === "lighter" || f === "aster";
26
+ }
27
+ /** The adapter itself when it implements TradingV2 (ondo, arcus, qfex); else the legacy wrapper. */
28
+ /** cancelAllV2 verification: extra open-order re-lists (and the wait between them) for acked cancels still listed. */
29
+ const CANCEL_ALL_RELIST_RETRIES = 4;
30
+ const CANCEL_ALL_RELIST_DELAY_MS = 1_000;
31
+ export function tradingV2Of(adapter, network) {
32
+ if (isTradingV2(adapter))
33
+ return adapter;
34
+ return toTradingV2(adapter, network);
35
+ }
36
+ export function toTradingV2(adapter, network) {
37
+ const venue = getVenue(adapter.name);
38
+ if (!isLegacyFamily(venue.family)) {
39
+ throw new PerpError("NOT_IMPLEMENTED", `${venue.displayName} adapter does not implement TradingV2`);
40
+ }
41
+ return new LegacyTradingV2(adapter, venue, venue.family, requireCapabilities(venue), network);
42
+ }
43
+ /**
44
+ * Place one reduce-only TP/SL trigger on a separateOrders venue (hyperliquid, lighter /
45
+ * lighter-rh, aster) and normalize the result. HL tags the trigger "tp"/"sl" natively;
46
+ * Lighter / Aster receive the leg kind explicitly and send take-profit / stop-loss as given.
47
+ */
48
+ export async function placeTpSlLeg(adapter, network, leg) {
49
+ const venue = getVenue(adapter.name);
50
+ // A TradingV2 venue with separate TP/SL orders (QFEX) places its own legs.
51
+ const own = adapter.placeTpSlLeg;
52
+ if (isTradingV2(adapter) && typeof own === "function" && requireCapabilities(venue).tpsl === "separateOrders") {
53
+ return own.call(adapter, leg);
54
+ }
55
+ if (!isLegacyFamily(venue.family) || requireCapabilities(venue).tpsl !== "separateOrders") {
56
+ throw new PerpError("INVALID_PARAMS", `${venue.displayName} does not place TP/SL as separate trigger orders`);
57
+ }
58
+ const w = new LegacyTradingV2(adapter, venue, venue.family, requireCapabilities(venue), network);
59
+ try {
60
+ return await w.placeTpSlLeg(leg);
61
+ }
62
+ catch (e) {
63
+ throw builderRejectionOf(adapter, e); // never retried
64
+ }
65
+ }
66
+ function rec(v) {
67
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
68
+ }
69
+ function plainDecimal(v) {
70
+ const s = typeof v === "number" && Number.isFinite(v) ? String(v) : v;
71
+ return typeof s === "string" && /^\d+(\.\d+)?$/.test(s) ? s : null;
72
+ }
73
+ function positiveDecimal(v) {
74
+ const s = plainDecimal(v);
75
+ return s !== null && Number(s) > 0 ? s : null;
76
+ }
77
+ function idString(v) {
78
+ if (typeof v === "number" && Number.isSafeInteger(v))
79
+ return String(v);
80
+ if (typeof v === "string" && v !== "")
81
+ return v;
82
+ return null;
83
+ }
84
+ /**
85
+ * "filled" when `filled` covers the size SENT (half a lot when the market lot `step` is known,
86
+ * else float-noise tolerance), "cancelled" for 0, else "partiallyFilled".
87
+ */
88
+ function fillStatus(size, filled, step) {
89
+ if (Number(filled) === 0)
90
+ return "cancelled";
91
+ return Number(size) - Number(filled) <= iocLotTolerance(size, filled, step) ? "filled" : "partiallyFilled";
92
+ }
93
+ const PENDING = { status: "accepted", finality: "pending", orderId: null, filledSize: null, avgPrice: null };
94
+ /** HL `order` / `batchModify` response: response.data.statuses[0] = resting | filled | error. */
95
+ function parseHl(raw, size, step) {
96
+ const statuses = rec(rec(rec(raw)?.response)?.data)?.statuses;
97
+ // Trigger orders are acknowledged with the bare status string "waitingForTrigger" (no oid).
98
+ if (Array.isArray(statuses) && statuses[0] === "waitingForTrigger") {
99
+ // The oid the adapter identified in frontendOpenOrders after the ack (HyperliquidAdapter.triggerOrder).
100
+ return { status: "open", finality: "final", orderId: idString(rec(raw)?.triggerOid), filledSize: "0", avgPrice: null };
101
+ }
102
+ const st = Array.isArray(statuses) ? rec(statuses[0]) : null;
103
+ if (st === null)
104
+ return PENDING;
105
+ const resting = rec(st.resting);
106
+ if (resting !== null) {
107
+ return { status: "open", finality: "final", orderId: idString(resting.oid), filledSize: "0", avgPrice: null };
108
+ }
109
+ const filled = rec(st.filled);
110
+ const total = filled !== null ? plainDecimal(filled.totalSz) : null;
111
+ if (filled !== null && total !== null) {
112
+ return { status: fillStatus(size, total, step), finality: "final", orderId: idString(filled.oid), filledSize: total, avgPrice: positiveDecimal(filled.avgPx) };
113
+ }
114
+ return PENDING;
115
+ }
116
+ const ASTER_STATUS = {
117
+ NEW: "open", PARTIALLY_FILLED: "partiallyFilled", FILLED: "filled",
118
+ CANCELED: "cancelled", EXPIRED: "cancelled", REJECTED: "rejected",
119
+ };
120
+ /** Aster order object (POST/PUT /fapi/v3/order): orderId, status, executedQty, avgPrice. */
121
+ function parseAster(raw) {
122
+ const r = rec(raw);
123
+ if (r === null)
124
+ return PENDING;
125
+ const orderId = idString(r.orderId);
126
+ const status = typeof r.status === "string" ? ASTER_STATUS[r.status] : undefined;
127
+ const executed = plainDecimal(r.executedQty);
128
+ if (status === undefined || executed === null)
129
+ return { ...PENDING, orderId };
130
+ return { status, finality: "final", orderId, filledSize: executed, avgPrice: positiveDecimal(r.avgPrice) };
131
+ }
132
+ /**
133
+ * Lighter resting / post-only placement: the adapter's resolved `orderOutcome` (tx event_info or
134
+ * accountOrders — LighterAdapter.resolveOrderOutcome) is final; without one the sendTx ack is pending.
135
+ */
136
+ function parseLighter(raw) {
137
+ const o = rec(rec(raw)?.orderOutcome);
138
+ if (o === null || o.final !== true)
139
+ return PENDING;
140
+ const status = o.status === "open" || o.status === "partiallyFilled" || o.status === "filled" || o.status === "cancelled" ? o.status : null;
141
+ const filled = plainDecimal(o.filledSize);
142
+ if (status === null || filled === null)
143
+ return PENDING;
144
+ return { status, finality: "final", orderId: idString(o.orderId), filledSize: filled, avgPrice: null };
145
+ }
146
+ /**
147
+ * Pacifica create response: { success, data: { order_id } } is an ack, never a status. Final
148
+ * only with the adapter's resolved `orderOutcome` (history_by_id — PacificaAdapter.resolveOrderOutcome).
149
+ */
150
+ function parsePacifica(raw) {
151
+ const r = rec(raw);
152
+ const data = rec(r?.data) ?? r;
153
+ const orderId = idString(data?.order_id);
154
+ const o = rec(r?.orderOutcome);
155
+ if (o === null || o.final !== true || idString(o.orderId) !== orderId)
156
+ return { ...PENDING, orderId };
157
+ const status = o.status === "open" || o.status === "partiallyFilled" || o.status === "filled" || o.status === "cancelled" || o.status === "rejected" ? o.status : null;
158
+ const filled = plainDecimal(o.filledSize);
159
+ if (status === null || filled === null)
160
+ return { ...PENDING, orderId };
161
+ return { status, finality: "final", orderId, filledSize: filled, avgPrice: null };
162
+ }
163
+ // ── Wrapper ──
164
+ class LegacyTradingV2 {
165
+ adapter;
166
+ venue;
167
+ family;
168
+ caps;
169
+ network;
170
+ name;
171
+ constructor(adapter, venue, family, caps, network) {
172
+ this.adapter = adapter;
173
+ this.venue = venue;
174
+ this.family = family;
175
+ this.caps = caps;
176
+ this.network = network;
177
+ this.name = venue.id;
178
+ }
179
+ getVenueMarkets() {
180
+ return this.venue.publicData.fetchMarkets(this.network);
181
+ }
182
+ async placeOrder(req) {
183
+ validateOrderRequest(this.venue, this.caps, req);
184
+ let r;
185
+ try {
186
+ if (req.type === "market")
187
+ r = await this.placeMarket(req);
188
+ else if (req.type === "limit")
189
+ r = await this.placeLimit(req);
190
+ else
191
+ r = await this.placeStop(req);
192
+ }
193
+ catch (e) {
194
+ // A builder-related rejection drops the cached approval and says how to proceed; never retried.
195
+ throw builderRejectionOf(this.adapter, e);
196
+ }
197
+ return this.withBuilderFee(r, req.symbol);
198
+ }
199
+ /**
200
+ * The builder decision the adapter applied to an order on `symbol` — the same cached
201
+ * gate decision (builder-fee.ts), so the record matches what was sent. HL needs the
202
+ * asset index (HIP-3 assets never carry it).
203
+ */
204
+ async builderDecision(symbol) {
205
+ if (builderGateOf(this.adapter) === null)
206
+ return NO_BUILDER; // not configured (default): nothing to record
207
+ if (this.family === "hyperliquid") {
208
+ const hl = this.adapter;
209
+ return hl.builderDecision(await hl.getAssetIndex(symbol.toUpperCase()));
210
+ }
211
+ return builderDecisionOf(this.adapter, { hip3: false });
212
+ }
213
+ /** Record execution.builderFee (+ the reason a configured fee was not attached). */
214
+ async withBuilderFee(r, symbol) {
215
+ const d = await this.builderDecision(symbol);
216
+ r.execution.builderFee = d.applied;
217
+ if (d.note !== null)
218
+ r.execution.notes.push(d.note);
219
+ return r;
220
+ }
221
+ /** Lighter's numeric clientOrderIndex for the request (lighterClientOrderIndex rule). */
222
+ async lighterIndex(clientOrderId) {
223
+ if (this.family !== "lighter" || clientOrderId === null)
224
+ return undefined;
225
+ const { lighterClientOrderIndex } = await import("./lighter.js");
226
+ return lighterClientOrderIndex(clientOrderId);
227
+ }
228
+ /**
229
+ * The string client order id the adapter sends, validated there before sending: Pacifica
230
+ * `client_order_id` (full UUID), Hyperliquid `c` (128-bit hex cloid), Aster `newClientOrderId`.
231
+ */
232
+ stringClientId(clientOrderId) {
233
+ return this.family !== "lighter" && clientOrderId !== null ? clientOrderId : undefined;
234
+ }
235
+ parse(raw, size, step = null) {
236
+ switch (this.family) {
237
+ case "hyperliquid": return parseHl(raw, size, step);
238
+ case "aster": return parseAster(raw);
239
+ case "pacifica": return parsePacifica(raw);
240
+ case "lighter": return parseLighter(raw);
241
+ }
242
+ }
243
+ /**
244
+ * Build the result. clientOrderId is only what the venue order carries: Lighter's
245
+ * clientOrderIndex, Pacifica's client_order_id and Hyperliquid's cloid as the adapter sent
246
+ * them; Aster's `clientOrderId` echoed by the venue — only when one was sent (Aster generates
247
+ * one for every order otherwise, which perp-cli did not choose and does not report).
248
+ * The order was sent, so a response without any id is reported (note), never thrown.
249
+ */
250
+ result(req, raw, p, execution) {
251
+ const sent = req.clientOrderId ?? null;
252
+ let clientOrderId = null;
253
+ if (this.family === "lighter")
254
+ clientOrderId = idString(rec(raw)?.clientOrderIndex);
255
+ else if (this.family === "pacifica" || this.family === "hyperliquid")
256
+ clientOrderId = idString(rec(raw)?.clientOrderId);
257
+ else if (sent !== null) {
258
+ clientOrderId = idString(rec(raw)?.clientOrderId);
259
+ if (clientOrderId === null)
260
+ execution.notes.push(`the venue response does not echo the client order id ${sent} that was sent`);
261
+ else if (clientOrderId !== sent)
262
+ execution.notes.push(`the venue echoed client order id ${clientOrderId}, not the ${sent} that was sent`);
263
+ }
264
+ if (p.orderId === null && clientOrderId === null) {
265
+ execution.notes.push(`the venue response carries no order id — find the order with: perp account orders -e ${this.venue.id}`);
266
+ }
267
+ const r = withExecution({
268
+ venue: this.venue.id, symbol: req.symbol, status: p.status, finality: p.finality,
269
+ orderId: p.orderId, clientOrderId, filledSize: p.filledSize, avgPrice: p.avgPrice, venueRaw: raw,
270
+ }, execution.method, execution);
271
+ if (this.family === "lighter") {
272
+ const tx = rec(raw)?.tx_hash;
273
+ if (typeof tx === "string" && tx !== "")
274
+ r.execution.notes.push(`tx hash ${tx}`);
275
+ }
276
+ return r;
277
+ }
278
+ async placeMarket(req) {
279
+ if (req.slippage === null)
280
+ throw new PerpError("INVALID_PARAMS", `${this.venue.displayName} market order is missing its resolved slippage bound`);
281
+ const slippage = req.slippage;
282
+ const opts = { reduceOnly: req.reduceOnly, slippage };
283
+ const idx = await this.lighterIndex(req.clientOrderId);
284
+ if (idx !== undefined)
285
+ opts.clientOrderIndex = idx;
286
+ const cid = this.stringClientId(req.clientOrderId);
287
+ if (cid !== undefined)
288
+ opts.clientOrderId = cid;
289
+ const lot = await this.lotSize(req.symbol, req.size);
290
+ const raw = await this.adapter.marketOrder(req.symbol, req.side, lot.size, opts);
291
+ // The size is floored to the market lot before sending (HL echoes it as `raw.sentSize`):
292
+ // the fill is judged against what was sent, with half-lot tolerance.
293
+ const sentRaw = rec(raw)?.sentSize;
294
+ const sent = typeof sentRaw === "string" && sentRaw !== "" ? sentRaw : lot.size;
295
+ const parsed = this.parse(raw, sent, lot.step);
296
+ const outcome = isIocOutcomeCapable(this.adapter) ? this.adapter.iocOutcomeOf(raw) : null;
297
+ const p = outcome !== null
298
+ ? { ...parsed, status: fillStatus(sent, outcome.filledSize, lot.step), finality: "final", filledSize: outcome.filledSize }
299
+ : { ...parsed, status: "accepted", finality: "pending", filledSize: null };
300
+ const execution = this.marketExecution(raw, slippage);
301
+ if (sent !== req.size && Number(sent) !== Number(req.size))
302
+ execution.notes.push(`size floored to the venue lot: sent ${sent} (requested ${req.size})`);
303
+ if (outcome === null)
304
+ execution.notes.push("the venue response does not report a final fill — verify the position before acting again");
305
+ return this.result(req, raw, p, execution);
306
+ }
307
+ /** The market lot step (venue market info; unknown → throws before sending) and the size floored to it. */
308
+ async lotSize(symbol, size) {
309
+ const step = await lotStepOf(this.adapter, symbol);
310
+ return { step, size: floorSize(size, step) };
311
+ }
312
+ marketExecution(raw, slippage) {
313
+ const base = {
314
+ method: "protectedIoc", slippageBps: slippage.bps, slippageSource: slippage.source,
315
+ protectionPrice: null, referencePrice: null, tifRequested: null, tifSent: "IOC", expiresAt: null, builderFee: null, notes: [],
316
+ };
317
+ switch (this.family) {
318
+ case "hyperliquid": {
319
+ // HL marketOrder attaches the protection it sent (`raw.protection`); the venue does not echo it.
320
+ const pr = rec(rec(raw)?.protection);
321
+ if (pr === null)
322
+ return { ...base, tifSent: "Ioc", notes: ["protection record missing from the adapter response"] };
323
+ return {
324
+ ...base,
325
+ tifSent: "Ioc",
326
+ slippageBps: typeof pr.appliedBps === "number" ? Math.round(pr.appliedBps * 100) / 100 : null,
327
+ protectionPrice: positiveDecimal(pr.protectionPrice),
328
+ referencePrice: positiveDecimal(pr.referencePrice),
329
+ notes: [`reference l2Book mid; requested ${slippage.bps} bps, applied after HL price-grid rounding`],
330
+ };
331
+ }
332
+ case "aster": {
333
+ // Aster marketOrder attaches the protection it sent (`raw.protection`).
334
+ const pr = rec(rec(raw)?.protection);
335
+ if (pr === null) {
336
+ return { ...base, protectionPrice: positiveDecimal(rec(raw)?.price), notes: ["protection record missing from the adapter response; slippageBps = requested bound"] };
337
+ }
338
+ return {
339
+ ...base,
340
+ slippageBps: typeof pr.appliedBps === "number" ? Math.round(pr.appliedBps * 100) / 100 : null,
341
+ protectionPrice: positiveDecimal(pr.protectionPrice),
342
+ referencePrice: positiveDecimal(pr.referencePrice),
343
+ notes: [`IOC LIMIT at mark ± bound, tick-aligned toward crossing; requested ${slippage.bps} bps, applied after tick alignment`],
344
+ };
345
+ }
346
+ case "pacifica":
347
+ return {
348
+ ...base, method: "venueSlippage", tifSent: null,
349
+ notes: [`sent slippage_percent ${slippage.bps / 100}; Pacifica derives the protection price`],
350
+ };
351
+ case "lighter": {
352
+ const pr = rec(rec(raw)?.protection);
353
+ if (pr === null)
354
+ return { ...base, notes: ["protection record missing from the adapter response"] };
355
+ const applied = typeof pr.appliedBps === "number" ? pr.appliedBps : null;
356
+ return {
357
+ ...base,
358
+ slippageBps: applied,
359
+ protectionPrice: positiveDecimal(pr.protectionPrice),
360
+ referencePrice: positiveDecimal(pr.referencePrice),
361
+ notes: [`reference ${String(pr.referenceSource)}; requested ${slippage.bps} bps, applied after tick alignment`],
362
+ };
363
+ }
364
+ }
365
+ }
366
+ /** Venue-native TIF the adapter sends for a validated TIF, and the resting-order expiry it implies. */
367
+ nativeTif(tif) {
368
+ switch (this.family) {
369
+ case "hyperliquid":
370
+ return { tifSent: tif === "GTC" ? "Gtc" : tif === "IOC" ? "Ioc" : tif === "ALO" ? "Alo" : tif, expiresAt: null, notes: [] };
371
+ case "aster":
372
+ return { tifSent: tif === "ALO" || tif === "GTX" ? "GTX" : tif, expiresAt: null, notes: [] };
373
+ case "pacifica":
374
+ return { tifSent: tif, expiresAt: null, notes: [] };
375
+ case "lighter":
376
+ if (tif === "GTC" || tif === "GTT") {
377
+ return { tifSent: "GTT", expiresAt: Date.now() + LIGHTER_GTT_EXPIRY_MS, notes: ["GTC is sent as GTT with the venue's 28-day expiry"] };
378
+ }
379
+ if (tif === "ALO") {
380
+ return { tifSent: "POST_ONLY", expiresAt: Date.now() + LIGHTER_GTT_EXPIRY_MS, notes: ["ALO is sent as time-in-force POST_ONLY (2) with a 28-day expiry"] };
381
+ }
382
+ return { tifSent: tif, expiresAt: null, notes: [] };
383
+ }
384
+ }
385
+ async placeLimit(req) {
386
+ if (req.price === null || req.tif === null)
387
+ throw new PerpError("INVALID_PARAMS", "limit order requires a price and a time-in-force");
388
+ const opts = { reduceOnly: req.reduceOnly, tif: req.tif };
389
+ const idx = await this.lighterIndex(req.clientOrderId);
390
+ if (idx !== undefined)
391
+ opts.clientOrderIndex = idx;
392
+ const cid = this.stringClientId(req.clientOrderId);
393
+ if (cid !== undefined)
394
+ opts.clientOrderId = cid;
395
+ const lot = await this.lotSize(req.symbol, req.size);
396
+ const raw = await this.adapter.limitOrder(req.symbol, req.side, req.price, lot.size, opts);
397
+ const t = this.nativeTif(req.tif);
398
+ let parsed = this.parse(raw, lot.size, lot.step);
399
+ if (req.tif === "IOC") {
400
+ // IOC: the venue-final outcome when the adapter reports one (HL statuses, Lighter tx event_info).
401
+ const outcome = isIocOutcomeCapable(this.adapter) ? this.adapter.iocOutcomeOf(raw) : null;
402
+ if (outcome !== null)
403
+ parsed = { ...parsed, status: fillStatus(lot.size, outcome.filledSize, lot.step), finality: "final", filledSize: outcome.filledSize };
404
+ }
405
+ const notes = [...t.notes];
406
+ if (lot.size !== req.size && Number(lot.size) !== Number(req.size))
407
+ notes.push(`size floored to the venue lot: sent ${lot.size} (requested ${req.size})`);
408
+ return this.result(req, raw, parsed, {
409
+ method: "limit", slippageBps: null, slippageSource: null, protectionPrice: null, referencePrice: null,
410
+ tifRequested: req.tif, tifSent: t.tifSent, expiresAt: t.expiresAt, builderFee: null, notes,
411
+ });
412
+ }
413
+ async placeStop(req) {
414
+ if (req.triggerPrice === null)
415
+ throw new PerpError("INVALID_PARAMS", `${req.type} order requires a trigger price`);
416
+ if (req.triggerSource !== null && (this.family === "hyperliquid" || this.family === "lighter")) {
417
+ throw unsupported(this.venue.displayName, "triggerSource", "trigger orders fire on the venue's fixed price source", "Omit --trigger-type");
418
+ }
419
+ if (this.family !== "lighter" && req.clientOrderId !== null) {
420
+ throw unsupported(this.venue.displayName, "stopClientOrderId", "this CLI does not send a client order id with stop orders", "Omit the client order id for stop orders");
421
+ }
422
+ const isMarket = req.type === "stopMarket";
423
+ const opts = {
424
+ reduceOnly: req.reduceOnly,
425
+ };
426
+ if (req.price !== null)
427
+ opts.limitPrice = req.price;
428
+ if (req.triggerSource !== null)
429
+ opts.triggerType = req.triggerSource;
430
+ // Protection-priced market triggers (Lighter) bound the fill at trigger ± this; native triggers ignore it.
431
+ if (req.slippage !== null)
432
+ opts.slippage = req.slippage;
433
+ const idx = await this.lighterIndex(req.clientOrderId);
434
+ if (idx !== undefined)
435
+ opts.clientOrderIndex = idx;
436
+ const raw = await this.adapter.stopOrder(req.symbol, req.side, req.size, req.triggerPrice, opts);
437
+ const execution = {
438
+ method: isMarket ? "trigger" : "triggerLimit", slippageBps: null, slippageSource: null, protectionPrice: null,
439
+ referencePrice: req.triggerPrice, tifRequested: req.tif, tifSent: null, expiresAt: null, builderFee: null,
440
+ notes: req.tif !== null ? ["trigger orders use the venue's own time-in-force"] : [],
441
+ };
442
+ if (isMarket) {
443
+ const pr = this.family === "lighter" ? rec(rec(raw)?.protection) : null;
444
+ if (pr !== null) {
445
+ execution.slippageBps = typeof pr.appliedBps === "number" ? pr.appliedBps : null;
446
+ execution.slippageSource = pr.slippageSource === "flag" || pr.slippageSource === "venue" || pr.slippageSource === "default" ? pr.slippageSource : null;
447
+ execution.protectionPrice = positiveDecimal(pr.protectionPrice);
448
+ execution.notes.push("limit IOC at trigger ± bound once triggered — a fast move through the bound can leave it unfilled");
449
+ }
450
+ else {
451
+ execution.notes.push("venue-native market trigger (no slippage bound)");
452
+ }
453
+ }
454
+ if (this.family === "lighter") {
455
+ execution.expiresAt = Date.now() + LIGHTER_GTT_EXPIRY_MS;
456
+ }
457
+ if (this.family === "aster") {
458
+ // Plain stop: Aster classifies from the trigger vs the mark/last price (venue trigger rules).
459
+ const t = rec(raw)?.type ?? rec(raw)?.origType;
460
+ if (typeof t === "string" && t.startsWith("TAKE_PROFIT")) {
461
+ execution.notes.push(`sent as ${t}: the trigger is on the take-profit side of the current price (Aster fires STOP only beyond the price in the order's direction)`);
462
+ }
463
+ }
464
+ return this.result(req, raw, this.parse(raw, req.size), execution);
465
+ }
466
+ async placeTpSlLeg(leg) {
467
+ if (!(Number(leg.size) > 0))
468
+ throw new PerpError("INVALID_PARAMS", `TP/SL size must be positive (got "${leg.size}")`);
469
+ let raw;
470
+ if (this.family === "hyperliquid") {
471
+ const hl = this.adapter;
472
+ raw = await hl.triggerOrder(leg.symbol, leg.closeSide, leg.size, leg.trigger, leg.kind, {
473
+ isMarket: leg.limitPrice === null, ...(leg.limitPrice !== null ? { limitPrice: leg.limitPrice } : {}), reduceOnly: true, grouping: "na",
474
+ });
475
+ }
476
+ else {
477
+ // Explicit kind: Aster / Lighter send the take-profit or stop-loss type as given (no
478
+ // price-based classification); a trigger already crossed is rejected by the venue.
479
+ raw = await this.adapter.stopOrder(leg.symbol, leg.closeSide, leg.size, leg.trigger, {
480
+ ...(leg.limitPrice !== null ? { limitPrice: leg.limitPrice } : {}), reduceOnly: true, slippage: leg.slippage, kind: leg.kind,
481
+ });
482
+ }
483
+ const execution = {
484
+ method: leg.kind === "tp" ? "takeProfitTrigger" : "stopLossTrigger", slippageBps: null, slippageSource: null,
485
+ protectionPrice: leg.limitPrice, referencePrice: leg.trigger, tifRequested: null, tifSent: null, expiresAt: null, builderFee: null,
486
+ notes: ["reduce-only"],
487
+ };
488
+ const pr = this.family === "lighter" ? rec(rec(raw)?.protection) : null;
489
+ if (pr !== null) {
490
+ execution.slippageBps = typeof pr.appliedBps === "number" ? pr.appliedBps : null;
491
+ execution.slippageSource = leg.slippage.source;
492
+ execution.protectionPrice = positiveDecimal(pr.protectionPrice);
493
+ execution.notes.push("limit IOC at trigger ± bound once triggered — a fast move through the bound can leave it unfilled");
494
+ }
495
+ else if (leg.limitPrice === null) {
496
+ execution.notes.push("venue-native market trigger (no slippage bound)");
497
+ }
498
+ const parsed = this.parse(raw, leg.size);
499
+ if (this.family === "lighter") {
500
+ execution.expiresAt = Date.now() + LIGHTER_GTT_EXPIRY_MS;
501
+ // The sendTx ack is not a placement: the leg counts only with the venue-final outcome the
502
+ // adapter resolved (stopOrder → resolveOrderOutcome: tx rejected → ORDER_REJECTED already
503
+ // thrown; else the accountOrders row). Not final → ORDER_PENDING; cancelled → ORDER_REJECTED
504
+ // (no protection in place); the caller reports ORDER_PARTIAL.
505
+ const what = `${leg.kind.toUpperCase()} trigger ${leg.closeSide} ${leg.size} ${leg.symbol} @ ${leg.trigger}`;
506
+ if (parsed.finality !== "final") {
507
+ throw orderOutcomeUnknown(this.venue.id, what, new Error("the trigger order is not listed in accountOrders yet"), `Do NOT resubmit — check open orders: perp account orders -e ${this.venue.id}`);
508
+ }
509
+ const venueStatus = rec(rec(raw)?.orderOutcome)?.venueStatus;
510
+ if (parsed.status === "cancelled") {
511
+ throw new PerpError("ORDER_REJECTED", `${this.venue.displayName} ${what}: the venue cancelled the trigger order (${String(venueStatus)})`, { exchange: this.venue.id });
512
+ }
513
+ execution.notes.push(`venue status ${String(venueStatus)} (accountOrders / tx event_info)`);
514
+ }
515
+ return this.withBuilderFee(this.result({ symbol: leg.symbol }, raw, parsed, execution), leg.symbol);
516
+ }
517
+ async amendOrder(req) {
518
+ if (req.ref.kind !== "orderId") {
519
+ throw unsupported(this.venue.displayName, "amendByClientOrderId", "orders are edited by venue order id", `Look up the order id: perp account orders -e ${this.venue.id}`);
520
+ }
521
+ if (this.family === "pacifica")
522
+ return this.amendPacifica(req, req.ref.value);
523
+ if (req.tif !== null) {
524
+ throw unsupported(this.venue.displayName, "amendTimeInForce", "an edit keeps the resting order's own time-in-force / post-only", "Omit --tif");
525
+ }
526
+ const raw = await this.adapter.editOrder(req.symbol, req.ref.value, req.price, req.size);
527
+ const p = this.parse(raw, req.size);
528
+ const notes = ["size = new open (unfilled) size", "the order's TIF / post-only / reduce-only are kept"];
529
+ if (p.orderId === null)
530
+ notes.push(`the venue did not echo an order id — showing the edited order's id ${req.ref.value}`);
531
+ return this.result({ symbol: req.symbol }, raw, { ...p, orderId: p.orderId ?? req.ref.value }, {
532
+ method: "nativeAmend", slippageBps: null, slippageSource: null, protectionPrice: null, referencePrice: null,
533
+ tifRequested: null, tifSent: null, expiresAt: null, builderFee: null, notes,
534
+ });
535
+ }
536
+ /**
537
+ * Pacifica: the native edit re-creates the order as ALO whatever its TIF, so the TIF is
538
+ * kept by PacificaAdapter.amendKeepingTif (native edit only for ALO, else cancel→replace).
539
+ */
540
+ async amendPacifica(req, orderId) {
541
+ const a = this.adapter;
542
+ if (typeof a.amendKeepingTif !== "function") {
543
+ throw new PerpError("NOT_IMPLEMENTED", "Pacifica adapter has no TIF-preserving amend");
544
+ }
545
+ const { pacificaTif } = await import("./pacifica.js");
546
+ const tif = req.tif === null ? null : pacificaTif(req.tif);
547
+ const r = await a.amendKeepingTif(req.symbol, orderId, req.price, req.size, tif, req.forceTif === true);
548
+ const notes = ["size = new open (unfilled) size", ...r.notes];
549
+ if (r.method === "cancelReplace") {
550
+ notes.push(`filledBeforeCancel ${r.filledBeforeCancel ?? "unknown"}, replacedSize ${r.replacedSize ?? "none"}`);
551
+ }
552
+ const replaced = r.method === "nativeAmend" || r.replacedSize !== null;
553
+ let orderIdOut = r.orderId;
554
+ if (orderIdOut === null && r.clientOrderId === null) {
555
+ // No replacement placed (filled / nothing left): the result refers to the cancelled original.
556
+ orderIdOut = orderId;
557
+ }
558
+ if (r.method === "nativeAmend" && r.orderId === null)
559
+ notes.push(`the venue did not echo the new order id — find it with: perp account orders -e ${this.venue.id}`);
560
+ // Not replaced: the original filled completely (status filled, with its fill), or it was
561
+ // cancelled with a partial fill (partiallyFilled) / nothing (cancelled).
562
+ const filledOut = r.filledBeforeCancel ?? "0";
563
+ // Replaced: final only with the edited / replacement order read back (history_by_id, price / size verified).
564
+ const o = replaced ? r.outcome : null;
565
+ if (replaced && o === null)
566
+ notes.push(`the ${r.method === "nativeAmend" ? "edited" : "replacement"} order's state was not confirmed — verify: perp account orders -e ${this.venue.id}`);
567
+ if (o !== null)
568
+ notes.push(`venue order ${o.orderId}: ${o.venueStatus} @ ${o.price ?? "?"} x ${o.size ?? "?"} (history_by_id)`);
569
+ const status = replaced ? (o !== null ? o.status : "accepted")
570
+ : r.filledCompletely ? "filled"
571
+ : Number(filledOut) > 0 ? "partiallyFilled" : "cancelled";
572
+ const out = withExecution({
573
+ venue: this.venue.id, symbol: req.symbol, status, finality: replaced && o === null ? "pending" : "final",
574
+ orderId: orderIdOut, clientOrderId: r.clientOrderId, filledSize: replaced ? (o !== null ? o.filledSize : null) : filledOut, avgPrice: null, venueRaw: r.venueRaw,
575
+ }, r.method, { tifRequested: req.tif, tifSent: replaced ? r.tif : null, notes });
576
+ // Only a cancel→replace places a new order (createLimitOrder carries builder_code); a native edit does not.
577
+ return r.method === "cancelReplace" && r.replacedSize !== null ? this.withBuilderFee(out, req.symbol) : out;
578
+ }
579
+ /**
580
+ * Cancel one order. `cancelled` only with per-order proof from the venue result:
581
+ * HL statuses[0] "success"; Aster the order object with status CANCELED for that id;
582
+ * Pacifica `success: true` for the single-order request; Lighter the cancel tx EXECUTED.
583
+ * A definite refusal → `failed`; anything else → `pending` with a remediation.
584
+ */
585
+ async cancelByRef(symbol, ref, listedType = null) {
586
+ if (this.family === "pacifica" && ref.kind === "clientOrderId")
587
+ return this.cancelPacificaByClientId(symbol, ref);
588
+ if (this.family === "lighter" && ref.kind === "clientOrderId")
589
+ return this.cancelLighterByClientId(symbol, ref);
590
+ // Pacifica stop / TP / SL orders (incl. the position-level TP/SL that `--tp/--sl` and `trade tpsl`
591
+ // create: order_type take_profit_market / stop_loss_market, price 0, amount 0) are cancelled on
592
+ // POST /orders/stop/cancel (cancel_stop_order); /orders/cancel refuses them with 422 "Failed to
593
+ // cancel order" (live testnet 2026-10-02). `listedType` = the order_type from the open-orders listing.
594
+ // No listed type given (`trade cancel <symbol> <id>`): read it from the open orders, so a
595
+ // stop / TP / SL id is routed to the stop-cancel endpoint too. A failed read throws (nothing sent).
596
+ if (this.family === "pacifica" && listedType === null) {
597
+ const listed = (await this.freshOpenOrders()).find(o => o.orderId === ref.value);
598
+ if (listed !== undefined)
599
+ listedType = listed.type;
600
+ }
601
+ const pacStop = this.family === "pacifica" && listedType !== null && isPacificaStopType(listedType);
602
+ // Lighter: an order index above 2^53 is refused by the adapter (INVALID_PARAMS), never cancel-all.
603
+ let raw;
604
+ let refusal = null;
605
+ try {
606
+ if (ref.kind === "clientOrderId") {
607
+ // Native cancel by client order id: HL `cancelByCloid`, Aster DELETE /fapi/v3/order origClientOrderId.
608
+ raw = this.family === "hyperliquid"
609
+ ? await this.adapter.cancelByCloid(symbol, ref.value)
610
+ : await this.adapter.cancelOrderByClientId(symbol, ref.value);
611
+ }
612
+ else {
613
+ raw = pacStop
614
+ ? await this.adapter.cancelStopOrder(symbol, ref.value)
615
+ : await this.adapter.cancelOrder(symbol, ref.value);
616
+ }
617
+ }
618
+ catch (e) {
619
+ // Pacifica HTTP 4xx (e.g. 422 "Failed to cancel order" code 5 for an already-filled order):
620
+ // a definite refusal of THIS request — settled from the order's history_by_id, never "cancelled".
621
+ if (this.family !== "pacifica" || !pacificaRefused(e))
622
+ throw e;
623
+ raw = { error: e instanceof Error ? e.message : String(e) };
624
+ refusal = await this.adapter.cancelRefusalOutcome(ref.value, e);
625
+ }
626
+ const out = (v, reason = "") => {
627
+ if (v === "cancelled")
628
+ return { venue: this.venue.id, cancelled: [ref], failed: [], pending: [], venueRaw: raw };
629
+ if (v === "pending") {
630
+ return {
631
+ venue: this.venue.id, cancelled: [], failed: [], venueRaw: raw,
632
+ pending: [{ ref, reason, remediation: `Verify before acting again: perp account orders -e ${this.venue.id}` }],
633
+ };
634
+ }
635
+ return { venue: this.venue.id, cancelled: [], failed: [{ ref, error: v.error }], pending: [], venueRaw: raw };
636
+ };
637
+ const r = rec(raw);
638
+ switch (this.family) {
639
+ case "hyperliquid": {
640
+ // Per-entry {error} already threw ORDER_REJECTED in the adapter.
641
+ const st = rec(rec(r?.response)?.data)?.statuses;
642
+ return Array.isArray(st) && st[0] === "success" ? out("cancelled") : out("pending", "the venue response carries no per-order \"success\"");
643
+ }
644
+ case "aster":
645
+ return r !== null && r.status === "CANCELED" && idString(ref.kind === "orderId" ? r.orderId : r.clientOrderId) === ref.value
646
+ ? out("cancelled") : out("pending", "the venue response does not show this order CANCELED");
647
+ case "pacifica":
648
+ if (refusal !== null)
649
+ return refusal.kind === "failed" ? out({ error: refusal.error }) : out("pending", refusal.reason);
650
+ if (r !== null && (r.success === false || (typeof r.error === "string" && r.error !== "")))
651
+ return out({ error: String(r.error ?? "cancel rejected") });
652
+ return r !== null && r.success === true ? out("cancelled") : out("pending", "the venue response carries no success flag");
653
+ case "lighter": {
654
+ const lt = this.adapter;
655
+ if (ref.kind !== "orderId")
656
+ throw new PerpError("INVALID_PARAMS", "unreachable: a Lighter client order id is resolved to its order id first");
657
+ try {
658
+ await lt.awaitTxExecuted(raw, `cancel ${ref.value} ${symbol}`);
659
+ }
660
+ catch (e) {
661
+ if (e instanceof PerpError && e.structured.code === "ORDER_REJECTED")
662
+ return out({ error: e.message });
663
+ return out("pending", e instanceof Error ? e.message : String(e));
664
+ }
665
+ // Tx executed: confirm the order itself is canceled / gone (accountOrders / open orders).
666
+ const st = await lt.cancelOutcome(raw, symbol, ref.value);
667
+ if (st === "cancelled")
668
+ return out("cancelled");
669
+ if (st === "filled")
670
+ return out({ error: `order ${ref.value} filled before the cancel` });
671
+ return out("pending", st === "open" ? "cancel tx executed but the order is still listed open" : "cancel tx executed but the order's canceled state could not be read");
672
+ }
673
+ }
674
+ }
675
+ /**
676
+ * Pacifica cancel by client order id (docs.pacifica.fi rest-api orders/cancel-order:
677
+ * `order_id` "required (if no CLOID)", `client_order_id` "Full UUID string, required (if no OID)").
678
+ * The id is resolved to the venue order id from GET /orders (rows carry `client_order_id`), then
679
+ * cancelled through the per-order path above — same stop routing and per-order proof — and
680
+ * reported under the client reference the caller gave. Not open → ORDER_NOT_FOUND (nothing sent).
681
+ */
682
+ async cancelPacificaByClientId(symbol, ref) {
683
+ const pac = this.adapter;
684
+ const found = await pac.orderByClientId(ref.value);
685
+ if (found === null || !found.open) {
686
+ throw new PerpError("ORDER_NOT_FOUND", `Pacifica has no open order with client order id ${ref.value}${found !== null ? ` (order ${found.orderId} is no longer open)` : ""}`, {
687
+ exchange: "pacifica", remediation: "List open orders: perp account orders -e pacifica",
688
+ });
689
+ }
690
+ const r = await this.cancelByRef(symbol, { kind: "orderId", value: found.orderId }, found.type);
691
+ const mine = (x) => ({ ...x, ref });
692
+ return { ...r, cancelled: r.cancelled.map(() => ref), failed: r.failed.map(mine), pending: (r.pending ?? []).map(mine) };
693
+ }
694
+ /**
695
+ * Lighter / Lighter RH cancel by client order index. lighter-go L2CancelOrderTxInfo: `Index int64
696
+ * // Client Order Index or Order Index of the order to cancel` (client indexes 1..2^48 − 1, order
697
+ * indexes above). The index is resolved to the order's row (accountOrders?client_order_indexes —
698
+ * LighterAdapter.orderStatusRow) and cancelled through the per-order path above (the cancel is
699
+ * signed with that client index — lighterOrderSignIndex — and proven by the tx + the order's
700
+ * final state), reported under the client reference the caller gave. Not listed / not open →
701
+ * ORDER_NOT_FOUND (nothing sent).
702
+ */
703
+ async cancelLighterByClientId(symbol, ref) {
704
+ const { lighterOrderIdOf } = await import("./lighter.js");
705
+ const lt = this.adapter;
706
+ const row = await lt.orderStatusRow(symbol, ref);
707
+ const orderId = lighterOrderIdOf(row);
708
+ if (orderId === "") {
709
+ throw new PerpError("EXCHANGE_ERROR", `${this.venue.displayName} order row for client order index ${ref.value} carries no order id; nothing was cancelled`, {
710
+ exchange: this.venue.id, remediation: `List open orders: perp account orders -e ${this.venue.id}`,
711
+ });
712
+ }
713
+ // Open or not is decided by the per-order path: an order no longer among the market's active
714
+ // orders → ORDER_NOT_FOUND there (LighterAdapter.activeOrderRow), nothing sent.
715
+ const r = await this.cancelByRef(symbol, { kind: "orderId", value: orderId });
716
+ const mine = (x) => ({ ...x, ref });
717
+ return { ...r, cancelled: r.cancelled.map(() => ref), failed: r.failed.map(mine), pending: (r.pending ?? []).map(mine) };
718
+ }
719
+ /**
720
+ * Pacifica per-order status: the order's `/orders/history_by_id` rows (PacificaAdapter.orderStatusById).
721
+ * A client order id is first resolved to the venue order id (orderByClientId: open orders, then
722
+ * the latest order-history page). Rows that prove no state yet → accepted / pending.
723
+ */
724
+ async pacificaOrderStatus(symbol, ref) {
725
+ const pac = this.adapter;
726
+ let orderId = ref.value;
727
+ let listedSymbol = null;
728
+ if (ref.kind === "clientOrderId") {
729
+ const found = await pac.orderByClientId(ref.value);
730
+ if (found === null) {
731
+ throw new PerpError("ORDER_NOT_FOUND", `Pacifica has no order with client order id ${ref.value} among the open orders and the latest order-history page`, {
732
+ exchange: "pacifica", remediation: "Query by venue order id instead: perp trade status <orderId> -e pacifica (ids: perp account orders / perp history orders -e pacifica)",
733
+ });
734
+ }
735
+ orderId = found.orderId;
736
+ listedSymbol = found.symbol;
737
+ }
738
+ const st = await pac.orderStatusById(orderId);
739
+ const o = st.outcome;
740
+ return withExecution({
741
+ venue: this.venue.id, symbol: listedSymbol ?? symbol, status: o === null ? "accepted" : o.status, finality: o === null ? "pending" : "final",
742
+ orderId, clientOrderId: ref.kind === "clientOrderId" ? ref.value : st.clientOrderId,
743
+ filledSize: o === null ? null : o.filledSize, avgPrice: null, venueRaw: st.venueRaw,
744
+ }, "statusQuery", { notes: [o === null ? "history_by_id shows no settled row for this order yet" : `venue status "${o.venueStatus}" (history_by_id)`] });
745
+ }
746
+ /** Open orders read live: drops the adapter's in-process snapshot first (Aster keeps a 5s one). */
747
+ async freshOpenOrders() {
748
+ const a = this.adapter;
749
+ if (typeof a.invalidateOpenOrdersCache === "function")
750
+ a.invalidateOpenOrdersCache();
751
+ return this.adapter.getOpenOrders();
752
+ }
753
+ /**
754
+ * Cancel every open order (of `symbol`): one cancelByRef per order listed open now, so each
755
+ * result carries per-order proof. Afterwards the open orders are re-listed live: any order
756
+ * still open is reported `pending` (never `cancelled`), whatever the per-order result said.
757
+ * Orders placed after the first listing and still open at the re-list are reported pending too.
758
+ */
759
+ async cancelAllV2(symbol) {
760
+ const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
761
+ const inScope = (o) => symbol === null || norm(o.symbol) === norm(symbol);
762
+ const remediation = `Verify before acting again: perp account orders -e ${this.venue.id}`;
763
+ const targets = (await this.freshOpenOrders()).filter(inScope);
764
+ const res = { venue: this.venue.id, cancelled: [], failed: [], pending: [], venueRaw: [] };
765
+ for (const o of targets) {
766
+ const ref = { kind: "orderId", value: o.orderId };
767
+ try {
768
+ const r = await this.cancelByRef(o.symbol, ref, o.type);
769
+ res.cancelled.push(...r.cancelled);
770
+ res.failed.push(...r.failed);
771
+ res.pending.push(...(r.pending ?? []));
772
+ res.venueRaw.push(r.venueRaw);
773
+ }
774
+ catch (e) {
775
+ if (e instanceof PerpError && (e.structured.code === "ORDER_REJECTED" || e.structured.code === "INVALID_PARAMS")) {
776
+ res.failed.push({ ref, error: e.message });
777
+ }
778
+ else {
779
+ res.pending.push({ ref, reason: e instanceof Error ? e.message : String(e), remediation });
780
+ }
781
+ }
782
+ }
783
+ if (targets.length === 0)
784
+ return res;
785
+ // Verify: re-list live. A read failure leaves every "cancelled" unconfirmed (pending), not assumed.
786
+ // A venue's open-orders list can lag its own cancel ack (Pacifica mainnet listed an order whose
787
+ // cancel had returned success for ~1 s): re-list a few times before calling an acked cancel pending.
788
+ let stillOpen;
789
+ try {
790
+ const acked = new Set(res.cancelled.map(ref => ref.value));
791
+ for (let attempt = 0;; attempt++) {
792
+ stillOpen = (await this.freshOpenOrders()).filter(inScope).map(o => o.orderId);
793
+ if (attempt >= CANCEL_ALL_RELIST_RETRIES || !stillOpen.some(id => acked.has(id)))
794
+ break;
795
+ await new Promise(r => setTimeout(r, CANCEL_ALL_RELIST_DELAY_MS));
796
+ }
797
+ }
798
+ catch (e) {
799
+ const reason = `post-cancel open-orders re-list failed: ${e instanceof Error ? e.message : String(e)}`;
800
+ res.pending.push(...res.cancelled.map(ref => ({ ref, reason, remediation })));
801
+ res.cancelled = [];
802
+ return res;
803
+ }
804
+ // Every order of the first listing already has an outcome (cancelled / failed / pending);
805
+ // only orders NOT in it are "not covered". (Excluding the cancelled ones here reported an
806
+ // order acked-cancelled-but-still-open twice, once with the wrong "not covered" reason.)
807
+ const known = new Set(targets.map(o => o.orderId));
808
+ const open = new Set(stillOpen);
809
+ const confirmed = res.cancelled.filter(ref => !open.has(ref.value));
810
+ for (const ref of res.cancelled) {
811
+ if (open.has(ref.value))
812
+ res.pending.push({ ref, reason: "still listed open after the cancel", remediation });
813
+ }
814
+ res.cancelled = confirmed;
815
+ for (const id of stillOpen) {
816
+ if (!known.has(id)) {
817
+ res.pending.push({ ref: { kind: "orderId", value: id }, reason: "still listed open after cancel-all (not covered by the first listing)", remediation });
818
+ }
819
+ }
820
+ return res;
821
+ }
822
+ async applyLeverage(symbol, leverage, marginMode) {
823
+ if (marginMode !== null && !this.caps.marginModes.includes(marginMode)) {
824
+ throw unsupported(this.venue.displayName, `${marginMode}Margin`, `${marginMode} margin is not offered`, `Use one of: ${this.caps.marginModes.join(", ")}`);
825
+ }
826
+ // No mode given: the legacy setLeverage calls send a margin mode with every leverage update
827
+ // (HL / Pacifica / Lighter would apply cross), which could silently switch an isolated
828
+ // position. The current mode is not read back uniformly, so an explicit mode is required —
829
+ // unless the venue offers a single mode (nothing to switch).
830
+ let mode;
831
+ if (marginMode !== null)
832
+ mode = marginMode;
833
+ else if (this.caps.marginModes.length === 1)
834
+ mode = this.caps.marginModes[0];
835
+ else {
836
+ throw new PerpError("INVALID_PARAMS", `${this.venue.displayName}: a leverage update without a margin mode could switch the symbol's current mode; pass cross or isolated explicitly`, {
837
+ remediation: `Pass the margin mode: one of ${this.caps.marginModes.join(", ")}`,
838
+ });
839
+ }
840
+ const raw = await this.adapter.setLeverage(symbol, leverage, mode);
841
+ return { venue: this.venue.id, symbol, leverage, marginMode: mode, venueRaw: raw };
842
+ }
843
+ /**
844
+ * Lighter / Lighter RH per-order status from the venue's order row (LighterAdapter.orderStatusRow:
845
+ * accountOrders by client order index, or the market's active / inactive orders by order id).
846
+ * Status and fill come only from the row's `status` / `filled_base_amount` (lighterRowOutcomeOf);
847
+ * a row that is not final yet (in-progress) or carries no readable fill → accepted / pending.
848
+ */
849
+ async lighterOrderStatus(symbol, ref) {
850
+ const { lighterRowOutcomeOf, lighterOrderIdOf } = await import("./lighter.js");
851
+ const lt = this.adapter;
852
+ const row = await lt.orderStatusRow(symbol, ref);
853
+ const id = lighterOrderIdOf(row);
854
+ const orderId = id === "" ? null : id;
855
+ const out = lighterRowOutcomeOf(row, orderId);
856
+ const clientOrderId = idString(row.client_order_id) ?? idString(row.client_order_index);
857
+ const venueStatus = typeof row.status === "string" ? row.status : String(row.status);
858
+ return withExecution({
859
+ venue: this.venue.id, symbol, status: out === null ? "accepted" : out.status, finality: out === null ? "pending" : "final",
860
+ orderId, clientOrderId: clientOrderId === "0" ? null : clientOrderId,
861
+ filledSize: out === null ? null : out.filledSize, avgPrice: null, venueRaw: row,
862
+ }, "statusQuery", { notes: [`venue status "${venueStatus}"`] });
863
+ }
864
+ async getOrderStatus(symbol, ref) {
865
+ if (this.family === "lighter")
866
+ return this.lighterOrderStatus(symbol, ref);
867
+ if (this.family === "pacifica")
868
+ return this.pacificaOrderStatus(symbol, ref);
869
+ if (this.family === "aster")
870
+ return this.asterOrderStatus(symbol, ref);
871
+ if (this.family !== "hyperliquid" || !isQueryOrderCapable(this.adapter)) {
872
+ throw unsupported(this.venue.displayName, "orderStatus", "the adapter has no single-order status query", `List orders and positions: perp account orders -e ${this.venue.id} / perp account positions -e ${this.venue.id}`);
873
+ }
874
+ if (ref.kind === "orderId" && !/^\d+$/.test(ref.value)) {
875
+ throw new PerpError("INVALID_PARAMS", `Hyperliquid order status needs a numeric order id or c:<cloid> (got ${ref.value})`);
876
+ }
877
+ // info orderStatus `oid`: "Either u64 representing the order id or 16-byte hex string representing the client order id".
878
+ const label = ref.kind === "orderId" ? ref.value : `c:${ref.value}`;
879
+ const raw = await this.adapter.queryOrder(ref.kind === "orderId" ? Number(ref.value) : ref.value);
880
+ const r = rec(raw);
881
+ if (r?.status === "unknownOid") {
882
+ throw new PerpError("ORDER_NOT_FOUND", `Hyperliquid order ${label} not found`, { exchange: "hyperliquid" });
883
+ }
884
+ const wrap = rec(r?.order);
885
+ const o = rec(wrap?.order);
886
+ const st = wrap?.status;
887
+ if (r?.status !== "order" || o === null || typeof st !== "string") {
888
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid orderStatus ${label}: unexpected response shape`, { exchange: "hyperliquid" });
889
+ }
890
+ const orig = plainDecimal(o.origSz);
891
+ const left = plainDecimal(o.sz);
892
+ let filled = null;
893
+ if (orig !== null && left !== null) {
894
+ const dec = Math.max((orig.split(".")[1] ?? "").length, (left.split(".")[1] ?? "").length);
895
+ filled = (Number(orig) - Number(left)).toFixed(dec);
896
+ }
897
+ let status;
898
+ let finality = "final";
899
+ if (st === "open")
900
+ status = filled !== null && Number(filled) > 0 ? "partiallyFilled" : "open";
901
+ else if (st === "filled")
902
+ status = "filled";
903
+ else if (st === "rejected" || st.endsWith("Rejected"))
904
+ status = "rejected";
905
+ else if (st === "canceled" || st.endsWith("Canceled"))
906
+ status = "cancelled";
907
+ else {
908
+ status = "accepted";
909
+ finality = "pending";
910
+ }
911
+ const orderId = idString(o.oid);
912
+ if (orderId === null && ref.kind === "clientOrderId") {
913
+ throw new PerpError("EXCHANGE_ERROR", `Hyperliquid orderStatus ${label}: the order row carries no oid`, { exchange: "hyperliquid" });
914
+ }
915
+ const cloid = typeof o.cloid === "string" && o.cloid !== "" ? o.cloid : null;
916
+ return withExecution({
917
+ // No --symbol is needed for a cloid query (orderStatus is per user): the row's coin names the market.
918
+ venue: this.venue.id, symbol: symbol !== "" ? symbol : String(o.coin ?? ""), status, finality,
919
+ orderId: orderId ?? ref.value, clientOrderId: cloid,
920
+ filledSize: filled, avgPrice: null, venueRaw: raw,
921
+ }, "statusQuery", { notes: filled === null ? [`venue status "${st}"; filled size not reported`] : [`venue status "${st}"`] });
922
+ }
923
+ /**
924
+ * Aster per-order status from GET /fapi/v3/order (AsterAdapter.queryOrderRow: by orderId or
925
+ * origClientOrderId). Status / fill only from the row's `status` / `executedQty` (parseAster);
926
+ * an unmapped status or unreadable executedQty → accepted / pending.
927
+ */
928
+ async asterOrderStatus(symbol, ref) {
929
+ const ast = this.adapter;
930
+ const row = await ast.queryOrderRow(symbol, ref);
931
+ const p = parseAster(row);
932
+ const clientOrderId = idString(row.clientOrderId);
933
+ return withExecution({
934
+ venue: this.venue.id, symbol, status: p.status, finality: p.finality,
935
+ orderId: p.orderId ?? (ref.kind === "orderId" ? ref.value : null),
936
+ clientOrderId: ref.kind === "clientOrderId" ? clientOrderId ?? ref.value : clientOrderId,
937
+ filledSize: p.filledSize, avgPrice: p.avgPrice, venueRaw: row,
938
+ }, "statusQuery", { notes: [`venue status "${String(row.status)}" (GET /fapi/v3/order)`] });
939
+ }
940
+ }