perp-cli 0.12.17 → 0.14.1

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 (363) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +303 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  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 +357 -459
  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 +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  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 +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  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 +1386 -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 +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -234,6 +234,7 @@ function connect() {
234
234
 
235
235
  // ── Helpers ──
236
236
  function fmt(v, d=2) { const n=Number(v)||0; return n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); }
237
+ function usdOrDash(v, d=2) { return v==null ? '-' : '$'+fmt(v,d); }
237
238
  function pnlClass(v) { return Number(v)>=0?'pnl-pos':'pnl-neg'; }
238
239
  function pnlSign(v) { const n=Number(v)||0; return n>=0?'+$'+fmt(Math.abs(n)):'-$'+fmt(Math.abs(n)); }
239
240
  function rateClass(v) { const n=Number(v)||0; return n>0?'rate-positive':n<0?'rate-negative':'rate-neutral'; }
@@ -310,7 +311,7 @@ function renderPanels(exchanges) {
310
311
  const notional = Math.abs(Number(p.size)) * Number(p.markPrice);
311
312
  const margin = p.leverage > 0 ? notional / p.leverage : notional;
312
313
  const roe = margin > 0 ? (Number(p.unrealizedPnl) / margin * 100) : 0;
313
- return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>$\${fmt(p.entryPrice)}</td><td>$\${fmt(p.markPrice)}</td><td>\${p.liquidationPrice==='N/A'?'N/A':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">$\${fmt(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlSign(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe>=0?'+':''}\${fmt(roe,1)}%</td><td>\${p.leverage}x</td></tr>\`;
314
+ return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${p.side}">\${p.side.toUpperCase()}</td><td>\${p.size}</td><td>$\${fmt(p.entryPrice)}</td><td>$\${fmt(p.markPrice)}</td><td>\${(p.liquidationPrice==null||p.liquidationPrice==='N/A')?'-':'$'+fmt(p.liquidationPrice)}</td><td style="color:var(--muted)">$\${fmt(notional)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${pnlSign(p.unrealizedPnl)}</td><td class="\${pnlClass(p.unrealizedPnl)}">\${roe>=0?'+':''}\${fmt(roe,1)}%</td><td>\${p.leverage}x</td></tr>\`;
314
315
  }).join('')}</tbody></table>\`:'<div class="empty-msg">No open positions</div>'}
315
316
  <div class="section-title">Open Orders (\${filteredOrders.length})</div>
316
317
  \${filteredOrders.length?\`<table><thead><tr>\${isHL?'<th>DEX</th>':''}<th>Symbol</th><th>Side</th><th>Type</th><th>Price</th><th>Size</th><th>Filled</th><th>Status</th></tr></thead><tbody>\${filteredOrders.map(o=>{
@@ -319,7 +320,7 @@ function renderPanels(exchanges) {
319
320
  return \`<tr>\${isHL?\`<td style="color:var(--purple);font-size:11px">\${dex}</td>\`:''}<td>\${sym}</td><td class="side-\${o.side}">\${o.side.toUpperCase()}</td><td>\${o.type}</td><td>$\${fmt(o.price)}</td><td>\${o.size}</td><td>\${o.filled}</td><td>\${o.status}</td></tr>\`;
320
321
  }).join('')}</tbody></table>\`:'<div class="empty-msg">No open orders</div>'}
321
322
  <div class="section-title">Markets (Top 10)</div>
322
- \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td>$\${fmt(m.markPrice)}</td><td>$\${fmt(m.indexPrice)}</td><td class="\${rateClass(fr)}">\${(fr*100).toFixed(4)}%</td><td>$\${fmt(m.volume24h,0)}</td><td>$\${fmt(m.openInterest,0)}</td><td>\${m.maxLeverage}x</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
323
+ \${ex.topMarkets.length?\`<table><thead><tr><th>Symbol</th><th>Mark</th><th>Index</th><th>Funding</th><th>24h Vol</th><th>OI</th><th>Max Lev</th></tr></thead><tbody>\${ex.topMarkets.map(m=>{const fr=Number(m.fundingRate);return\`<tr><td>\${m.symbol}</td><td>$\${fmt(m.markPrice)}</td><td>\${usdOrDash(m.indexPrice)}</td><td class="\${rateClass(fr)}">\${(fr*100).toFixed(4)}%</td><td>\${usdOrDash(m.volume24h,0)}</td><td>\${usdOrDash(m.openInterest,0)}</td><td>\${m.maxLeverage}x</td></tr>\`;}).join('')}</tbody></table>\`:'<div class="empty-msg">No market data</div>'}
323
324
  </div>\`;
324
325
  }).join('');
325
326
 
@@ -345,9 +346,10 @@ function renderPanels(exchanges) {
345
346
  function renderArb() {
346
347
  if (!arbData) { $('#arb-table').innerHTML='<div class="empty-msg">Loading arb data...</div>'; return; }
347
348
 
348
- // Status
349
+ // Status (hover shows the venue's error message)
350
+ const escapeHtml = (v) => String(v).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[c]);
349
351
  const statusHtml = Object.entries(arbData.exchangeStatus||{}).map(([ex,st])=>
350
- \`<span class="exchange-status"><span class="dot-sm \${st==='ok'?'dot-ok':'dot-err'}"></span>\${ex}</span>\`
352
+ \`<span class="exchange-status" title="\${escapeHtml((arbData.exchangeErrors||{})[ex]||'')}"><span class="dot-sm \${st==='ok'?'dot-ok':'dot-err'}"></span>\${ex}</span>\`
351
353
  ).join('');
352
354
  $('#arb-status').innerHTML = statusHtml;
353
355
 
@@ -223,7 +223,7 @@ class HyperliquidFeed extends ExchangeWsFeed {
223
223
  size: String(Math.abs(szi)),
224
224
  entryPrice: pos.entryPx ?? "0",
225
225
  markPrice: String(pos.positionValue ? (Number(pos.positionValue) / Math.abs(szi)).toFixed(2) : "0"),
226
- liquidationPrice: pos.liquidationPx ?? "N/A",
226
+ liquidationPrice: pos.liquidationPx ?? null, // HL sends null when there is no liquidation price
227
227
  unrealizedPnl: pos.unrealizedPnl ?? "0",
228
228
  leverage: Number(ap.leverage?.value ?? 1),
229
229
  };
@@ -241,6 +241,7 @@ class HyperliquidFeed extends ExchangeWsFeed {
241
241
  filled: "0",
242
242
  status: "open",
243
243
  type: String(o.orderType ?? "limit"),
244
+ triggerPrice: o.isTrigger === true && Number(o.triggerPx) > 0 ? String(o.triggerPx) : null,
244
245
  }));
245
246
  }
246
247
  this.emitUpdate();
@@ -407,7 +408,7 @@ class PacificaFeed extends ExchangeWsFeed {
407
408
  size: String(size),
408
409
  entryPrice: String(entryPrice),
409
410
  markPrice: mark ?? "0",
410
- liquidationPrice: String(p.l ?? p.liquidation_price ?? "N/A"),
411
+ liquidationPrice: p.l != null ? String(p.l) : p.liquidation_price != null ? String(p.liquidation_price) : null,
411
412
  unrealizedPnl: pnl.toFixed(4),
412
413
  leverage: Number(p.leverage ?? 1),
413
414
  };
@@ -428,6 +429,7 @@ class PacificaFeed extends ExchangeWsFeed {
428
429
  filled: String(o.f ?? o.filled ?? "0"),
429
430
  status: "open",
430
431
  type: String(o.ot ?? o.order_type ?? ""),
432
+ triggerPrice: Number(o.sp ?? o.stop_price) > 0 ? String(o.sp ?? o.stop_price) : null,
431
433
  }));
432
434
  this.emitUpdate();
433
435
  }
@@ -437,8 +439,8 @@ class PacificaFeed extends ExchangeWsFeed {
437
439
  super.close();
438
440
  }
439
441
  }
440
- // ── Lighter WS Feed ──
441
- // WS: wss://mainnet.zklighter.elliot.ai/stream
442
+ // ── Lighter WS Feed (lighter, lighter-rh) ──
443
+ // WS: the adapter's venue+network config (venues/lighter-family.ts); null → REST
442
444
  // Auth: signer.createAuthToken() passed as `auth` field in subscribe messages
443
445
  // Channels: account_all_positions/{index} (auth), account_all_orders/{index} (auth),
444
446
  // user_stats/{index} (public — balance info)
@@ -449,7 +451,16 @@ class LighterFeed extends ExchangeWsFeed {
449
451
  const adapter = exchange.adapter;
450
452
  this.accountIndex = Number(adapter._accountIndex ?? -1);
451
453
  }
452
- get wsUrl() { return "wss://mainnet.zklighter.elliot.ai/stream"; }
454
+ /** Venue+network WS URL from the adapter's network config; null = none published. */
455
+ get configuredWsUrl() {
456
+ return this.exchange.adapter.network.wsUrl;
457
+ }
458
+ get wsUrl() {
459
+ const url = this.configuredWsUrl;
460
+ if (url === null)
461
+ throw new Error(`${this.exchange.name}: no WebSocket URL for this network`);
462
+ return url;
463
+ }
453
464
  async getAuthToken() {
454
465
  try {
455
466
  const adapter = this.exchange.adapter;
@@ -464,7 +475,7 @@ class LighterFeed extends ExchangeWsFeed {
464
475
  }
465
476
  }
466
477
  async connect() {
467
- if (this.accountIndex < 0) {
478
+ if (this.accountIndex < 0 || this.configuredWsUrl === null) {
468
479
  this.startRestFallback();
469
480
  return;
470
481
  }
@@ -566,7 +577,7 @@ class LighterFeed extends ExchangeWsFeed {
566
577
  markPrice: posSize !== 0
567
578
  ? String((Number(p.position_value ?? 0) / Math.abs(posSize)).toFixed(4))
568
579
  : "0",
569
- liquidationPrice: String(p.liquidation_price || "N/A"),
580
+ liquidationPrice: p.liquidation_price ? String(p.liquidation_price) : null,
570
581
  unrealizedPnl: String(p.unrealized_pnl ?? "0"),
571
582
  leverage: Number(p.initial_margin_fraction ?? 0) > 0
572
583
  ? Math.round(10000 / Number(p.initial_margin_fraction))
@@ -592,6 +603,7 @@ class LighterFeed extends ExchangeWsFeed {
592
603
  filled: String(o.filled_base_amount ?? "0"),
593
604
  status: "open",
594
605
  type: String(o.type ?? "limit"),
606
+ triggerPrice: Number(o.trigger_price) > 0 ? String(o.trigger_price) : null,
595
607
  }));
596
608
  this.emitUpdate();
597
609
  }
@@ -613,7 +625,7 @@ export class WsFeedManager {
613
625
  else if (ex.name === "pacifica") {
614
626
  feed = new PacificaFeed(ex, handler);
615
627
  }
616
- else if (ex.name === "lighter") {
628
+ else if (ex.name === "lighter" || ex.name === "lighter-rh") {
617
629
  feed = new LighterFeed(ex, handler);
618
630
  }
619
631
  else {
@@ -13,7 +13,7 @@
13
13
  * SILVERJM ≠ SILVER
14
14
  * URNM ≠ URANIUM
15
15
  */
16
- const HL_INFO_URL = "https://api.hyperliquid.xyz/info";
16
+ import { HYPERLIQUID_API_URL, sharedApiNetwork } from "./api/public/urls.js";
17
17
  /** Known aliases: different symbol → same underlying */
18
18
  const ALIASES = {
19
19
  // Crude oil (WTI benchmark)
@@ -45,7 +45,8 @@ function isBlacklisted(a, b) {
45
45
  return BLACKLIST_PAIRS.has(key1) || BLACKLIST_PAIRS.has(key2);
46
46
  }
47
47
  async function hlInfoPost(body) {
48
- const res = await fetch(HL_INFO_URL, {
48
+ // Selected network (review L7): HYPERLIQUID_API_URL follows -n via setSharedApiNetwork (live binding).
49
+ const res = await fetch(HYPERLIQUID_API_URL, {
49
50
  method: "POST",
50
51
  headers: { "Content-Type": "application/json" },
51
52
  body: JSON.stringify(body),
@@ -59,7 +60,8 @@ async function hlInfoPost(body) {
59
60
  */
60
61
  export async function fetchAllDexAssets() {
61
62
  const { withCache, TTL_MARKET } = await import("./cache.js");
62
- return withCache("pub:hl:allDexAssets", TTL_MARKET, () => _fetchAllDexAssetsLive());
63
+ // Network in the key: a mainnet snapshot must never answer a testnet run (L7).
64
+ return withCache(`pub:hl:${sharedApiNetwork()}:allDexAssets`, TTL_MARKET, () => _fetchAllDexAssetsLive());
63
65
  }
64
66
  async function _fetchAllDexAssetsLive() {
65
67
  const allMetas = await hlInfoPost({ type: "allPerpMetas" });
package/dist/errors.d.ts CHANGED
@@ -50,6 +50,11 @@ export declare const ERROR_CODES: {
50
50
  readonly status: 409;
51
51
  readonly retryable: false;
52
52
  };
53
+ readonly DEPOSIT_REQUIRED: {
54
+ readonly code: "DEPOSIT_REQUIRED";
55
+ readonly status: 403;
56
+ readonly retryable: false;
57
+ };
53
58
  readonly NO_SIGNER_AVAILABLE: {
54
59
  readonly code: "NO_SIGNER_AVAILABLE";
55
60
  readonly status: 401;
@@ -101,11 +106,73 @@ export declare const ERROR_CODES: {
101
106
  readonly status: 401;
102
107
  readonly retryable: false;
103
108
  };
109
+ readonly INVALID_PASSPHRASE: {
110
+ readonly code: "INVALID_PASSPHRASE";
111
+ readonly status: 401;
112
+ readonly retryable: false;
113
+ };
104
114
  readonly NOT_IMPLEMENTED: {
105
115
  readonly code: "NOT_IMPLEMENTED";
106
116
  readonly status: 501;
107
117
  readonly retryable: false;
108
118
  };
119
+ readonly UNKNOWN_VENUE: {
120
+ readonly code: "UNKNOWN_VENUE";
121
+ readonly status: 400;
122
+ readonly retryable: false;
123
+ };
124
+ readonly TIF_REQUIRED: {
125
+ readonly code: "TIF_REQUIRED";
126
+ readonly status: 400;
127
+ readonly retryable: false;
128
+ };
129
+ readonly INVALID_TICK: {
130
+ readonly code: "INVALID_TICK";
131
+ readonly status: 400;
132
+ readonly retryable: false;
133
+ };
134
+ readonly NO_ENV_TIER: {
135
+ readonly code: "NO_ENV_TIER";
136
+ readonly status: 401;
137
+ readonly retryable: false;
138
+ };
139
+ readonly NO_BRIDGE_ROUTE: {
140
+ readonly code: "NO_BRIDGE_ROUTE";
141
+ readonly status: 400;
142
+ readonly retryable: false;
143
+ };
144
+ readonly CAPABILITY_UNSUPPORTED: {
145
+ readonly code: "CAPABILITY_UNSUPPORTED";
146
+ readonly status: 400;
147
+ readonly retryable: false;
148
+ };
149
+ /** Part of a multi-leg placement was accepted and part failed; retrying would duplicate the accepted legs. */
150
+ readonly ORDER_PARTIAL: {
151
+ readonly code: "ORDER_PARTIAL";
152
+ readonly status: 500;
153
+ readonly retryable: false;
154
+ };
155
+ /** The order was submitted but its outcome is not confirmed; retrying could double-trade. */
156
+ readonly ORDER_PENDING: {
157
+ readonly code: "ORDER_PENDING";
158
+ readonly status: 202;
159
+ readonly retryable: false;
160
+ };
161
+ /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent (design §12.3). */
162
+ readonly ORDER_REJECTED: {
163
+ readonly code: "ORDER_REJECTED";
164
+ readonly status: 422;
165
+ readonly retryable: false;
166
+ };
167
+ /**
168
+ * The venue explicitly refused a non-order account action (withdraw / transfer / approval) and
169
+ * executed nothing — distinct from a network / unknown outcome (EXCHANGE_ERROR, UNKNOWN).
170
+ */
171
+ readonly ACTION_REJECTED: {
172
+ readonly code: "ACTION_REJECTED";
173
+ readonly status: 422;
174
+ readonly retryable: false;
175
+ };
109
176
  readonly EXCHANGE_UNREACHABLE: {
110
177
  readonly code: "EXCHANGE_UNREACHABLE";
111
178
  readonly status: 503;
@@ -175,3 +242,58 @@ export declare class PerpError extends Error {
175
242
  readonly structured: StructuredError;
176
243
  constructor(code: ErrorCode, message: string, details?: Record<string, unknown>);
177
244
  }
245
+ /**
246
+ * Codes that mean "the order was definitely not placed / filled nothing": a validation
247
+ * or policy failure before sending (incl. PRICE_STALE: no price to build the order), or an
248
+ * explicit venue rejection. Every other code
249
+ * (EXCHANGE_UNREACHABLE, TIMEOUT, EXCHANGE_ERROR, RATE_LIMITED, UNKNOWN, …) raised by
250
+ * an order call leaves the outcome unknown.
251
+ */
252
+ export declare const DEFINITE_ORDER_FAILURE_CODES: ReadonlySet<ErrorCode>;
253
+ /**
254
+ * true when `e` is a PerpError that says the order was definitely not placed: by its code, or —
255
+ * whatever the code — by `details.sent === false`, the thrower's explicit statement that it
256
+ * refused locally and sent nothing (QFEX: POSITION_NOT_FOUND from the pre-send position read).
257
+ * POSITION_NOT_FOUND is deliberately NOT in DEFINITE_ORDER_FAILURE_CODES: classifyError() also
258
+ * derives it from free message text ("position" + "not found"), and Aster / Lighter test such
259
+ * derived codes against the set — a text match is not proof that nothing was placed.
260
+ */
261
+ export declare function isDefiniteOrderFailure(e: unknown): boolean;
262
+ /**
263
+ * Failure BEFORE an order was sent (resolution, validation, pricing, nonce, signing):
264
+ * definitely not placed. A PerpError that already carries a definite code is kept.
265
+ */
266
+ export declare function orderNotSent(venue: string, what: string, e: unknown): PerpError;
267
+ /**
268
+ * Failure AFTER an order was sent (transport error, timeout, 5xx, unreadable body):
269
+ * the outcome is unknown — ORDER_PENDING (non-retryable, never resubmit).
270
+ */
271
+ export declare function orderOutcomeUnknown(venue: string, what: string, e: unknown, remediation?: string): PerpError;
272
+ /**
273
+ * A plain Error an adapter throws AFTER the order request reached the venue with an
274
+ * HTTP 5xx: Hyperliquid `HL exchange API error (5xx): …` (exchange POST, unparseable
275
+ * body) and Pacifica `POST <path> failed (5xx): …`. The outcome is unknown, never a
276
+ * rejection.
277
+ */
278
+ export declare function isUnknownOutcomeServerError(e: unknown): boolean;
279
+ /** A thrown value that looks like a transport failure (fetch/network/timeout/unparseable body). */
280
+ export declare function isTransportFailure(e: unknown): boolean;
281
+ /**
282
+ * A fetch failure where no connection was ever established (DNS failure, connection refused,
283
+ * connect timeout — read from the error's `code` / `cause` chain): the request was not sent.
284
+ */
285
+ export declare function isConnectFailure(e: unknown): boolean;
286
+ /**
287
+ * Failure of the request that SENDS an order / cancel / withdraw / transfer / leverage change.
288
+ * No connection was established (isConnectFailure) → not sent (definite). Anything else
289
+ * (connection reset, timeout, unreadable body, 5xx) → outcome unknown: ORDER_PENDING, never the
290
+ * retryable EXCHANGE_UNREACHABLE / TIMEOUT. A PerpError that is already definite is kept.
291
+ */
292
+ export declare function sendFailure(venue: string, what: string, e: unknown, remediation?: string): PerpError;
293
+ /** Remediation for an unknown-outcome account action (not an order). */
294
+ export declare function unknownActionRemediation(venue: string, action: string): string;
295
+ /**
296
+ * Strict positive decimal amount ("10", "10.5"; no sign, exponent, or trailing text — "10abc"
297
+ * and "1e3" are refused). Returns the trimmed string.
298
+ */
299
+ export declare function parsePositiveDecimal(raw: string, what: string): string;
package/dist/errors.js CHANGED
@@ -11,6 +11,7 @@ export const ERROR_CODES = {
11
11
  SIZE_TOO_LARGE: { code: "SIZE_TOO_LARGE", status: 400, retryable: false },
12
12
  RISK_VIOLATION: { code: "RISK_VIOLATION", status: 403, retryable: false },
13
13
  DUPLICATE_ORDER: { code: "DUPLICATE_ORDER", status: 409, retryable: false },
14
+ DEPOSIT_REQUIRED: { code: "DEPOSIT_REQUIRED", status: 403, retryable: false },
14
15
  // Agent-wallet error codes (OWS / Phase 2a)
15
16
  NO_SIGNER_AVAILABLE: { code: "NO_SIGNER_AVAILABLE", status: 401, retryable: false },
16
17
  AGENT_NOT_REGISTERED: { code: "AGENT_NOT_REGISTERED", status: 401, retryable: false },
@@ -22,7 +23,26 @@ export const ERROR_CODES = {
22
23
  APPROVE_FAILED: { code: "APPROVE_FAILED", status: 500, retryable: false },
23
24
  LOCK_HELD: { code: "LOCK_HELD", status: 423, retryable: true, retryAfterMs: 5000 },
24
25
  PASSPHRASE_REQUIRED: { code: "PASSPHRASE_REQUIRED", status: 401, retryable: false },
26
+ INVALID_PASSPHRASE: { code: "INVALID_PASSPHRASE", status: 401, retryable: false },
25
27
  NOT_IMPLEMENTED: { code: "NOT_IMPLEMENTED", status: 501, retryable: false },
28
+ // Venue abstraction (docs/DESIGN-venue-abstraction.md §10)
29
+ UNKNOWN_VENUE: { code: "UNKNOWN_VENUE", status: 400, retryable: false },
30
+ TIF_REQUIRED: { code: "TIF_REQUIRED", status: 400, retryable: false },
31
+ INVALID_TICK: { code: "INVALID_TICK", status: 400, retryable: false },
32
+ NO_ENV_TIER: { code: "NO_ENV_TIER", status: 401, retryable: false },
33
+ NO_BRIDGE_ROUTE: { code: "NO_BRIDGE_ROUTE", status: 400, retryable: false },
34
+ CAPABILITY_UNSUPPORTED: { code: "CAPABILITY_UNSUPPORTED", status: 400, retryable: false },
35
+ /** Part of a multi-leg placement was accepted and part failed; retrying would duplicate the accepted legs. */
36
+ ORDER_PARTIAL: { code: "ORDER_PARTIAL", status: 500, retryable: false },
37
+ /** The order was submitted but its outcome is not confirmed; retrying could double-trade. */
38
+ ORDER_PENDING: { code: "ORDER_PENDING", status: 202, retryable: false },
39
+ /** The order was definitely NOT placed: the venue rejected it, or it failed before it was sent (design §12.3). */
40
+ ORDER_REJECTED: { code: "ORDER_REJECTED", status: 422, retryable: false },
41
+ /**
42
+ * The venue explicitly refused a non-order account action (withdraw / transfer / approval) and
43
+ * executed nothing — distinct from a network / unknown outcome (EXCHANGE_ERROR, UNKNOWN).
44
+ */
45
+ ACTION_REJECTED: { code: "ACTION_REJECTED", status: 422, retryable: false },
26
46
  // 5xx - System / transient errors
27
47
  EXCHANGE_UNREACHABLE: { code: "EXCHANGE_UNREACHABLE", status: 503, retryable: true },
28
48
  RATE_LIMITED: { code: "RATE_LIMITED", status: 429, retryable: true, retryAfterMs: 1000 },
@@ -117,12 +137,55 @@ export function classifyError(err, exchange) {
117
137
  if (lower.includes("passphrase required") || lower.includes("passphrase missing")) {
118
138
  return { ...ERROR_CODES.PASSPHRASE_REQUIRED, message, exchange };
119
139
  }
140
+ // OWS vault decrypt failure (NAPI binding throws "decryption failed: aead::Error"
141
+ // when the AEAD tag check fails — wrong passphrase or tampered ciphertext).
142
+ // Distinct from PASSPHRASE_REQUIRED (which means no passphrase was supplied):
143
+ // here a passphrase WAS supplied but did not unlock the vault.
144
+ if (lower.includes("aead::error") || lower.includes("decryption failed") || lower.includes("decryption error")) {
145
+ return {
146
+ ...ERROR_CODES.INVALID_PASSPHRASE,
147
+ message,
148
+ exchange,
149
+ remediation: "Re-check OWS passphrase. Confirm with: perp --json wallet show --passphrase $PP. If wallet was rekeyed, restore from backup or re-import.",
150
+ };
151
+ }
120
152
  if (lower.includes("partial") && (lower.includes("approve") || lower.includes("approval"))) {
121
153
  return { ...ERROR_CODES.APPROVE_PARTIAL, message, exchange };
122
154
  }
123
155
  if (lower.includes("approve_failed") || (lower.includes("approve failed") && lower.includes("clean"))) {
124
156
  return { ...ERROR_CODES.APPROVE_FAILED, message, exchange };
125
157
  }
158
+ // Aster V3 gates every authenticated endpoint behind a first main-wallet
159
+ // deposit as of 2026-09-01 (venue code -5050 DEPOSIT_REQUIRED, message
160
+ // "This function can only be used after deposit."). Without this branch the
161
+ // message falls through to a bare EXCHANGE_ERROR and the user never learns
162
+ // that a deposit — not a key/permission problem — is what blocks them. The
163
+ // agent-wallet and builder endpoints are exempt, so onboarding succeeds and
164
+ // the first read is what fails.
165
+ if (lower.includes("after deposit") || lower.includes("deposit_required") || lower.includes("-5050")) {
166
+ return {
167
+ ...ERROR_CODES.DEPOSIT_REQUIRED,
168
+ message,
169
+ exchange,
170
+ remediation: "This venue requires a completed deposit to the main wallet before any authenticated endpoint works. " +
171
+ "Deposit with: perp funds deposit aster <amount> (USDT on BNB Chain; --chain arbitrum / --asset USDC also work), " +
172
+ "or from the exchange UI, then retry. Public market data and agent-wallet setup are unaffected.",
173
+ };
174
+ }
175
+ // Aster -4061: the account is in Hedge Mode, where `positionSide` is
176
+ // mandatory and `reduceOnly` is forbidden. This adapter always sends one-way
177
+ // shape, so every order is rejected until the account switches back. Without
178
+ // this branch the user sees a bare "position side does not match" with no
179
+ // indication that the ACCOUNT MODE is the thing to change.
180
+ if (lower.includes("-4061") || lower.includes("position side does not match") || lower.includes("position_side_not_match")) {
181
+ return {
182
+ ...ERROR_CODES.INVALID_PARAMS,
183
+ message,
184
+ exchange,
185
+ remediation: "The account appears to be in Hedge Mode, which this adapter does not support (it sends one-way orders). " +
186
+ "Switch to One-way Mode in the exchange UI, or via POST /fapi/v3/positionSide/dual with dualSidePosition=false, then retry.",
187
+ };
188
+ }
126
189
  if (lower.includes("not found") && (lower.includes("symbol") || lower.includes("market") || lower.includes("asset"))) {
127
190
  return { ...ERROR_CODES.SYMBOL_NOT_FOUND, message, exchange };
128
191
  }
@@ -172,3 +235,134 @@ export class PerpError extends Error {
172
235
  };
173
236
  }
174
237
  }
238
+ // ── Order outcome classification (design §12.3) ──
239
+ /**
240
+ * Codes that mean "the order was definitely not placed / filled nothing": a validation
241
+ * or policy failure before sending (incl. PRICE_STALE: no price to build the order), or an
242
+ * explicit venue rejection. Every other code
243
+ * (EXCHANGE_UNREACHABLE, TIMEOUT, EXCHANGE_ERROR, RATE_LIMITED, UNKNOWN, …) raised by
244
+ * an order call leaves the outcome unknown.
245
+ */
246
+ export const DEFINITE_ORDER_FAILURE_CODES = new Set([
247
+ "INVALID_PARAMS", "SYMBOL_NOT_FOUND", "INSUFFICIENT_BALANCE", "MARGIN_INSUFFICIENT",
248
+ "SIZE_TOO_SMALL", "SIZE_TOO_LARGE", "RISK_VIOLATION", "DEPOSIT_REQUIRED",
249
+ "NO_SIGNER_AVAILABLE", "AGENT_NOT_REGISTERED", "AGENT_EXPIRED", "POLICY_DENIED",
250
+ "KEY_NOT_FOUND", "WALLET_LOCKED", "PASSPHRASE_REQUIRED", "INVALID_PASSPHRASE",
251
+ "NOT_IMPLEMENTED", "UNKNOWN_VENUE", "TIF_REQUIRED", "INVALID_TICK",
252
+ "NO_ENV_TIER", "CAPABILITY_UNSUPPORTED", "SIGNATURE_FAILED", "ORDER_REJECTED", "ACTION_REJECTED",
253
+ // No usable price to build/check the order — raised only before an order is sent.
254
+ "PRICE_STALE",
255
+ ]);
256
+ /**
257
+ * true when `e` is a PerpError that says the order was definitely not placed: by its code, or —
258
+ * whatever the code — by `details.sent === false`, the thrower's explicit statement that it
259
+ * refused locally and sent nothing (QFEX: POSITION_NOT_FOUND from the pre-send position read).
260
+ * POSITION_NOT_FOUND is deliberately NOT in DEFINITE_ORDER_FAILURE_CODES: classifyError() also
261
+ * derives it from free message text ("position" + "not found"), and Aster / Lighter test such
262
+ * derived codes against the set — a text match is not proof that nothing was placed.
263
+ */
264
+ export function isDefiniteOrderFailure(e) {
265
+ if (!(e instanceof PerpError))
266
+ return false;
267
+ return DEFINITE_ORDER_FAILURE_CODES.has(e.structured.code) || e.structured.details?.sent === false;
268
+ }
269
+ /**
270
+ * Failure BEFORE an order was sent (resolution, validation, pricing, nonce, signing):
271
+ * definitely not placed. A PerpError that already carries a definite code is kept.
272
+ */
273
+ export function orderNotSent(venue, what, e) {
274
+ // By CODE only: a not-sent error with a retryable code (TIMEOUT / EXCHANGE_UNREACHABLE with
275
+ // details.sent === false) is still re-typed, so an order call never surfaces a retryable code.
276
+ if (e instanceof PerpError && DEFINITE_ORDER_FAILURE_CODES.has(e.structured.code))
277
+ return e;
278
+ return new PerpError("ORDER_REJECTED", `${venue} ${what} not sent: ${extractErrorMessage(e)}`, { exchange: venue });
279
+ }
280
+ /**
281
+ * Failure AFTER an order was sent (transport error, timeout, 5xx, unreadable body):
282
+ * the outcome is unknown — ORDER_PENDING (non-retryable, never resubmit).
283
+ */
284
+ export function orderOutcomeUnknown(venue, what, e, remediation) {
285
+ if (e instanceof PerpError && e.structured.code === "ORDER_PENDING")
286
+ return e;
287
+ return new PerpError("ORDER_PENDING", `${venue} ${what}: outcome unknown — ${extractErrorMessage(e)}`, {
288
+ exchange: venue,
289
+ remediation: remediation ?? `Do NOT resubmit — the order may have been placed. Check open orders, balances and positions on ${venue}`,
290
+ });
291
+ }
292
+ /**
293
+ * A plain Error an adapter throws AFTER the order request reached the venue with an
294
+ * HTTP 5xx: Hyperliquid `HL exchange API error (5xx): …` (exchange POST, unparseable
295
+ * body) and Pacifica `POST <path> failed (5xx): …`. The outcome is unknown, never a
296
+ * rejection.
297
+ */
298
+ export function isUnknownOutcomeServerError(e) {
299
+ if (e instanceof PerpError)
300
+ return false;
301
+ return /^HL exchange API error \(5\d\d\)|^POST \S+ failed \(5\d\d\)/.test(extractErrorMessage(e));
302
+ }
303
+ /** A thrown value that looks like a transport failure (fetch/network/timeout/unparseable body). */
304
+ export function isTransportFailure(e) {
305
+ if (e instanceof PerpError)
306
+ return false;
307
+ if (e instanceof TypeError || e instanceof SyntaxError)
308
+ return true;
309
+ const name = e instanceof Error ? e.name : "";
310
+ if (name === "AbortError" || name === "TimeoutError")
311
+ return true;
312
+ // "network" alone is NOT a transport signal: definite refusals such as "wrong network" /
313
+ // "unsupported network" mention it too. Only transport phrasings ("network error",
314
+ // "NetworkError when attempting to fetch", "network request failed", …) count.
315
+ return /fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|socket hang up|\bnetwork ?(error|request failed|failure|unreachable|is unreachable|connection lost)\b|timed? ?out/i.test(extractErrorMessage(e));
316
+ }
317
+ const CONNECT_FAILURE_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "UND_ERR_CONNECT_TIMEOUT", "ENETUNREACH", "EHOSTUNREACH"]);
318
+ /**
319
+ * A fetch failure where no connection was ever established (DNS failure, connection refused,
320
+ * connect timeout — read from the error's `code` / `cause` chain): the request was not sent.
321
+ */
322
+ export function isConnectFailure(e) {
323
+ const seen = new Set();
324
+ const walk = (x) => {
325
+ if (x === null || typeof x !== "object" || seen.has(x))
326
+ return false;
327
+ seen.add(x);
328
+ const o = x;
329
+ if (typeof o.code === "string" && CONNECT_FAILURE_CODES.has(o.code))
330
+ return true;
331
+ if (Array.isArray(o.errors) && o.errors.length > 0 && o.errors.every(walk))
332
+ return true;
333
+ return walk(o.cause);
334
+ };
335
+ return walk(e);
336
+ }
337
+ /**
338
+ * Failure of the request that SENDS an order / cancel / withdraw / transfer / leverage change.
339
+ * No connection was established (isConnectFailure) → not sent (definite). Anything else
340
+ * (connection reset, timeout, unreadable body, 5xx) → outcome unknown: ORDER_PENDING, never the
341
+ * retryable EXCHANGE_UNREACHABLE / TIMEOUT. A PerpError that is already definite is kept.
342
+ */
343
+ export function sendFailure(venue, what, e, remediation) {
344
+ if (e instanceof PerpError && DEFINITE_ORDER_FAILURE_CODES.has(e.structured.code))
345
+ return e;
346
+ if (isDefiniteOrderFailure(e))
347
+ return orderNotSent(venue, what, e);
348
+ if (isConnectFailure(e))
349
+ return orderNotSent(venue, what, e);
350
+ return orderOutcomeUnknown(venue, what, e, remediation);
351
+ }
352
+ /** Remediation for an unknown-outcome account action (not an order). */
353
+ export function unknownActionRemediation(venue, action) {
354
+ return `Do NOT resubmit — the ${action} may have been executed. Check balances, positions and open orders on ${venue} first`;
355
+ }
356
+ /**
357
+ * Strict positive decimal amount ("10", "10.5"; no sign, exponent, or trailing text — "10abc"
358
+ * and "1e3" are refused). Returns the trimmed string.
359
+ */
360
+ export function parsePositiveDecimal(raw, what) {
361
+ const s = String(raw).trim();
362
+ if (!/^(\d+(\.\d+)?|\.\d+)$/.test(s) || !(Number(s) > 0)) {
363
+ throw new PerpError("INVALID_PARAMS", `${what} must be a positive decimal number (got "${raw}")`, {
364
+ remediation: "Pass a plain decimal amount, e.g. 10 or 10.5",
365
+ });
366
+ }
367
+ return s;
368
+ }