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,43 @@
1
+ /**
2
+ * Lighter-family public market data (`lighter`, `lighter-rh`) — design §10.6.
3
+ *
4
+ * Sources (REST base from venues/lighter-family.ts, per venue + network):
5
+ * GET /api/v1/orderBookDetails → perps in `order_book_details` (decimals, min
6
+ * amounts, status, mark/index price, IMF, OI, volume). It carries every
7
+ * field /orderBooks has, so /orderBooks is not fetched (one fewer call
8
+ * under the 60 req/min standard limit).
9
+ * GET /api/v1/funding-rates → MULTI-EXCHANGE aggregate (binance/bybit/
10
+ * hyperliquid/lighter). Only rows with exchange === "lighter" are the
11
+ * venue's own; the RH host labels its own rows "lighter" too (live
12
+ * 2026-09-30: 57 "lighter" rows = RH's 57 perps, matched by market_id).
13
+ *
14
+ * FUNDING: basis 8h per quoted rate, hourly settlement (design §10.1, Lighter
15
+ * and Lighter RH identical).
16
+ *
17
+ * ASSET CLASS: the API has no asset-class field (the undocumented
18
+ * `strategy_index` groups markets on Lighter main but is 0 for every RH market),
19
+ * so classification is an explicit per-venue table built from live listings on
20
+ * 2026-09-30:
21
+ * - lighter-rh: every listed perp is in RH_CLASS. An unlisted symbol is
22
+ * excluded with a warning until it is classified (RH lists mostly equities,
23
+ * so no class can be assumed).
24
+ * - lighter: crypto-centric venue. LIGHTER_NON_CRYPTO lists every non-crypto
25
+ * perp; any other symbol is crypto by this venue's definition.
26
+ * LIGHTER_AMBIGUOUS tickers are excluded with a warning so they cannot
27
+ * produce a false cross-venue match.
28
+ *
29
+ * OPEN INTEREST: `open_interest` in base units (same value legacy getMarkets reports).
30
+ */
31
+ import { PerpError } from "../errors.js";
32
+ import type { VenuePublicData } from "./venue.js";
33
+ import type { LighterVenueId } from "./venues/lighter-family.js";
34
+ /**
35
+ * The venue's firewall answering instead of the API: HTTP 405 / 403 with a non-JSON (HTML
36
+ * "Human Verification") body → typed RATE_LIMITED, never the HTML in the message; null for any
37
+ * other response. Live 2026-10-05: after a burst Lighter mainnet answers EVERY request (public
38
+ * GET /orderBooks included) this way for 1–2 minutes. Docs (apidocs.lighter.xyz/docs/rate-limits):
39
+ * a breached limit answers "HTTP 429, or HTTP 405"; cooldown "Firewall: 60 seconds, static".
40
+ * The API's own answers are JSON, so a JSON 405 / 403 is not a challenge.
41
+ */
42
+ export declare function lighterWafChallenge(venue: string, status: number, body: string): PerpError | null;
43
+ export declare function lighterPublicData(venue: LighterVenueId): VenuePublicData;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Lighter-family public market data (`lighter`, `lighter-rh`) — design §10.6.
3
+ *
4
+ * Sources (REST base from venues/lighter-family.ts, per venue + network):
5
+ * GET /api/v1/orderBookDetails → perps in `order_book_details` (decimals, min
6
+ * amounts, status, mark/index price, IMF, OI, volume). It carries every
7
+ * field /orderBooks has, so /orderBooks is not fetched (one fewer call
8
+ * under the 60 req/min standard limit).
9
+ * GET /api/v1/funding-rates → MULTI-EXCHANGE aggregate (binance/bybit/
10
+ * hyperliquid/lighter). Only rows with exchange === "lighter" are the
11
+ * venue's own; the RH host labels its own rows "lighter" too (live
12
+ * 2026-09-30: 57 "lighter" rows = RH's 57 perps, matched by market_id).
13
+ *
14
+ * FUNDING: basis 8h per quoted rate, hourly settlement (design §10.1, Lighter
15
+ * and Lighter RH identical).
16
+ *
17
+ * ASSET CLASS: the API has no asset-class field (the undocumented
18
+ * `strategy_index` groups markets on Lighter main but is 0 for every RH market),
19
+ * so classification is an explicit per-venue table built from live listings on
20
+ * 2026-09-30:
21
+ * - lighter-rh: every listed perp is in RH_CLASS. An unlisted symbol is
22
+ * excluded with a warning until it is classified (RH lists mostly equities,
23
+ * so no class can be assumed).
24
+ * - lighter: crypto-centric venue. LIGHTER_NON_CRYPTO lists every non-crypto
25
+ * perp; any other symbol is crypto by this venue's definition.
26
+ * LIGHTER_AMBIGUOUS tickers are excluded with a warning so they cannot
27
+ * produce a false cross-venue match.
28
+ *
29
+ * OPEN INTEREST: `open_interest` in base units (same value legacy getMarkets reports).
30
+ */
31
+ import { PerpError } from "../errors.js";
32
+ import { venueRateField } from "../utils/numeric.js";
33
+ import { acquireRate, rateLimitedError } from "./rate-limiter.js";
34
+ import { canonicalFor } from "./canonical-symbols.js";
35
+ import { lighterFamilyNetworks, lighterFamilyRateLimit } from "./venues/lighter-family.js";
36
+ const BASIS_8H = { kind: "perHours", hours: 8 };
37
+ const SETTLEMENT_INTERVAL_HOURS = 1;
38
+ // ── Classification tables (live listings 2026-09-30) ──
39
+ const RH_CLASS = {
40
+ // crypto
41
+ BTC: "crypto", ETH: "crypto", SOL: "crypto", HYPE: "crypto", ZEC: "crypto", LIT: "crypto",
42
+ VVV: "crypto", SUI: "crypto", NEAR: "crypto", XRP: "crypto", CASHCAT: "crypto", PONS: "crypto",
43
+ // commodity (canonical via EXPLICIT_CANONICAL: XAU → GOLD, XAG → SILVER)
44
+ XAU: "commodity", XAG: "commodity",
45
+ // etf
46
+ SPY: "etf", QQQ: "etf", SOXL: "etf", SLV: "etf", USO: "etf", SGOV: "etf",
47
+ // equity
48
+ AAPL: "equity", AMZN: "equity", GOOGL: "equity", META: "equity", MSFT: "equity", NVDA: "equity",
49
+ TSLA: "equity", AMD: "equity", INTC: "equity", MU: "equity", TSM: "equity", ORCL: "equity",
50
+ PLTR: "equity", COIN: "equity", CRCL: "equity", CRWV: "equity", BABA: "equity", SNDK: "equity",
51
+ SMCI: "equity", SOFI: "equity", AMC: "equity", RGTI: "equity", QBTS: "equity", CLSK: "equity",
52
+ IREN: "equity", WULF: "equity", LUNR: "equity", ASTS: "equity", BE: "equity", USAR: "equity",
53
+ SPCX: "equity", SKHY: "equity",
54
+ // pre-IPO / private (explicit-only canonical → null unless mapped)
55
+ OPENAI: "preIpo", ANTHROPIC: "preIpo", SHEIN: "preIpo", ANSEM: "preIpo", AI: "preIpo",
56
+ };
57
+ const LIGHTER_NON_CRYPTO = {
58
+ // commodity
59
+ XAU: "commodity", XAG: "commodity", XPD: "commodity", XPT: "commodity", XCU: "commodity",
60
+ WTI: "commodity", BRENTOIL: "commodity", NATGAS: "commodity", WHEAT: "commodity", URA: "commodity",
61
+ H100: "commodity",
62
+ // fx
63
+ EURUSD: "fx", GBPUSD: "fx", AUDUSD: "fx", NZDUSD: "fx", USDJPY: "fx", USDCAD: "fx",
64
+ USDCHF: "fx", USDHKD: "fx", USDKRW: "fx",
65
+ // index
66
+ US500: "index", US100: "index", US10Y: "index", KRCOMP: "index",
67
+ // etf
68
+ SPY: "etf", QQQ: "etf", IWM: "etf", DIA: "etf", SOXL: "etf", SOXS: "etf", SOXX: "etf",
69
+ BOTZ: "etf", MAGS: "etf", EWY: "etf", KORU: "etf",
70
+ // equity (US)
71
+ AAPL: "equity", AMZN: "equity", GOOGL: "equity", META: "equity", MSFT: "equity", NVDA: "equity",
72
+ TSLA: "equity", AMD: "equity", INTC: "equity", MU: "equity", TSM: "equity", ORCL: "equity",
73
+ PLTR: "equity", COIN: "equity", CRCL: "equity", CRWV: "equity", BABA: "equity", SNDK: "equity",
74
+ MSTR: "equity", ARM: "equity", NBIS: "equity", BMNR: "equity", CBRS: "equity", AVGO: "equity",
75
+ IBM: "equity", GEV: "equity", MRVL: "equity", HOOD: "equity", LITE: "equity", QCOM: "equity",
76
+ BE: "equity", AAOI: "equity", AXTI: "equity", GME: "equity", NOK: "equity", TTWO: "equity",
77
+ RKLB: "equity", MRNA: "equity", DELL: "equity", NOW: "equity", ASML: "equity", STRC: "equity",
78
+ SPCX: "equity", DRAM: "equity", WDC: "equity",
79
+ // equity (Asia)
80
+ SAMSUNG: "equity", SAMSUNGUSD: "equity", HYUNDAI: "equity", HYUNDAIUSD: "equity", HANMI: "equity",
81
+ SKHYNIX: "equity", SKHYNIXUSD: "equity", SKHY: "equity", BYD: "equity", TENCENT: "equity",
82
+ POPMART: "equity", KIOXIA: "equity", SMIC: "equity", XIAOMI: "equity",
83
+ // pre-IPO / private / unclear listing status (explicit-only canonical)
84
+ OPENAI: "preIpo", ANTHROPIC: "preIpo", SPACEX: "preIpo", SHEIN: "preIpo", CXMT: "preIpo",
85
+ ZHIPU: "preIpo", UNITREE: "preIpo", MINIMAX: "preIpo", AI: "preIpo", ANSEM: "preIpo",
86
+ STABLECOINX: "preIpo", OURA: "preIpo", PRL: "preIpo", ADI: "preIpo",
87
+ };
88
+ /**
89
+ * Excluded from cross-venue data until mapped explicitly:
90
+ * - BB, BOT, WEN, QNT: Lighter groups them with US equities, but each is also a live crypto ticker elsewhere.
91
+ * - SPX: the SPX6900 token on Lighter; its ticker equals the canonical key of the S&P 500 index ("SPX").
92
+ */
93
+ const LIGHTER_AMBIGUOUS = new Set(["BB", "BOT", "WEN", "QNT", "SPX"]);
94
+ /** Asset class for a venue symbol, or null when it must be excluded (unclassified / ambiguous). */
95
+ function classify(venue, symbol) {
96
+ const up = symbol.toUpperCase();
97
+ switch (venue) {
98
+ case "lighter-rh":
99
+ return RH_CLASS[up] ?? null;
100
+ case "lighter":
101
+ if (LIGHTER_AMBIGUOUS.has(up))
102
+ return null;
103
+ return LIGHTER_NON_CRYPTO[up] ?? "crypto"; // crypto-centric venue: non-crypto is listed explicitly
104
+ }
105
+ }
106
+ function warnExcluded(venue, what, symbols) {
107
+ if (symbols.length > 0) {
108
+ console.warn(`[${venue}] excluded ${symbols.length} ${what}: ${symbols.join(", ")}`);
109
+ }
110
+ }
111
+ // ── HTTP ──
112
+ /**
113
+ * The venue's firewall answering instead of the API: HTTP 405 / 403 with a non-JSON (HTML
114
+ * "Human Verification") body → typed RATE_LIMITED, never the HTML in the message; null for any
115
+ * other response. Live 2026-10-05: after a burst Lighter mainnet answers EVERY request (public
116
+ * GET /orderBooks included) this way for 1–2 minutes. Docs (apidocs.lighter.xyz/docs/rate-limits):
117
+ * a breached limit answers "HTTP 429, or HTTP 405"; cooldown "Firewall: 60 seconds, static".
118
+ * The API's own answers are JSON, so a JSON 405 / 403 is not a challenge.
119
+ */
120
+ export function lighterWafChallenge(venue, status, body) {
121
+ if (status !== 405 && status !== 403)
122
+ return null;
123
+ try {
124
+ JSON.parse(body);
125
+ return null;
126
+ }
127
+ catch { /* not JSON: the firewall's page */ }
128
+ return new PerpError("RATE_LIMITED", `${venue}: blocked by the venue's WAF challenge (HTTP ${status} Human Verification) — wait 1–2 minutes`, {
129
+ exchange: venue, retryAfterMs: 60_000,
130
+ remediation: `Wait 1–2 minutes before the next ${venue} request, then retry at a lower request rate (standard accounts: 60 requests per rolling minute)`,
131
+ });
132
+ }
133
+ async function getJson(venue, net, path, endpoint) {
134
+ await acquireRate(`${venue}:${net}`, lighterFamilyRateLimit(venue), endpoint);
135
+ const base = lighterFamilyNetworks(venue)[net].restUrl;
136
+ const res = await fetch(`${base}/api/v1${path}`, { signal: AbortSignal.timeout(15_000) });
137
+ const text = await res.text();
138
+ if (res.status === 429)
139
+ throw rateLimitedError(venue, res.headers.get("retry-after"), text);
140
+ const waf = lighterWafChallenge(venue, res.status, text);
141
+ if (waf !== null)
142
+ throw waf;
143
+ if (!res.ok) {
144
+ throw new PerpError("EXCHANGE_ERROR", `${venue} GET ${path} HTTP ${res.status}`, { body: text.slice(0, 500) });
145
+ }
146
+ try {
147
+ return JSON.parse(text);
148
+ }
149
+ catch {
150
+ throw new PerpError("EXCHANGE_ERROR", `${venue} GET ${path}: invalid JSON`, { body: text.slice(0, 500) });
151
+ }
152
+ }
153
+ function isNumStr(v) {
154
+ return typeof v === "string" && v.length > 0 && Number.isFinite(Number(v));
155
+ }
156
+ function validDetail(d) {
157
+ return typeof d.symbol === "string" && Number.isInteger(d.market_id)
158
+ && Number.isInteger(d.price_decimals) && d.price_decimals >= 0
159
+ && Number.isInteger(d.size_decimals) && d.size_decimals >= 0
160
+ && isNumStr(d.min_base_amount) && isNumStr(d.min_quote_amount)
161
+ && typeof d.min_initial_margin_fraction === "number" && d.min_initial_margin_fraction > 0
162
+ && isNumStr(d.mark_price) && typeof d.status === "string";
163
+ }
164
+ async function fetchPerpDetails(venue, net) {
165
+ const json = await getJson(venue, net, "/orderBookDetails", "orderBookDetails");
166
+ const list = json.order_book_details;
167
+ if (!Array.isArray(list)) {
168
+ throw new PerpError("EXCHANGE_ERROR", `${venue} orderBookDetails: missing order_book_details array`);
169
+ }
170
+ const out = [];
171
+ const malformed = [];
172
+ for (const d of list) {
173
+ if (d.market_type !== "perp")
174
+ continue;
175
+ if (!validDetail(d)) {
176
+ malformed.push(String(d.symbol));
177
+ continue;
178
+ }
179
+ out.push(d);
180
+ }
181
+ warnExcluded(venue, "malformed market(s)", malformed);
182
+ return out;
183
+ }
184
+ /** The venue's own funding rows, keyed by market_id. */
185
+ async function fetchOwnFunding(venue, net) {
186
+ const json = await getJson(venue, net, "/funding-rates", "funding-rates");
187
+ const list = json.funding_rates;
188
+ if (!Array.isArray(list)) {
189
+ throw new PerpError("EXCHANGE_ERROR", `${venue} funding-rates: missing funding_rates array`);
190
+ }
191
+ const map = new Map();
192
+ for (const r of list) {
193
+ if (r.exchange !== "lighter")
194
+ continue; // other exchanges' rates in the aggregate
195
+ if (!Number.isInteger(r.market_id) || typeof r.symbol !== "string" || typeof r.rate !== "number" || !Number.isFinite(r.rate)) {
196
+ throw new PerpError("EXCHANGE_ERROR", `${venue} funding-rates: malformed own row ${JSON.stringify(r).slice(0, 200)}`);
197
+ }
198
+ map.set(r.market_id, r);
199
+ }
200
+ return map;
201
+ }
202
+ function stepFromDecimals(d) {
203
+ return d === 0 ? "1" : `0.${"0".repeat(d - 1)}1`;
204
+ }
205
+ function toMarket(venue, net, d, assetClass, fr) {
206
+ const settle = lighterFamilyNetworks(venue)[net].collateral[0];
207
+ if (!settle)
208
+ throw new PerpError("INVALID_PARAMS", `${venue} ${net} network config has no collateral`);
209
+ return {
210
+ venue,
211
+ venueSymbol: String(d.market_id),
212
+ symbol: d.symbol,
213
+ canonical: canonicalFor(venue, d.symbol, assetClass),
214
+ assetClass,
215
+ settle,
216
+ quoteAsset: null,
217
+ markPrice: d.mark_price,
218
+ indexPrice: isNumStr(d.index_price) ? d.index_price : null,
219
+ fundingRate: fr ? venueRateField(fr.rate, "funding-rates rate", venue) : null,
220
+ fundingRateBasis: BASIS_8H,
221
+ settlementIntervalHours: SETTLEMENT_INTERVAL_HOURS,
222
+ openInterest: typeof d.open_interest === "number" ? String(d.open_interest) : null,
223
+ volume24h: typeof d.daily_quote_token_volume === "number" ? String(d.daily_quote_token_volume) : null,
224
+ tickSize: stepFromDecimals(d.price_decimals),
225
+ stepSize: stepFromDecimals(d.size_decimals),
226
+ minSize: d.min_base_amount,
227
+ minNotional: d.min_quote_amount,
228
+ maxLeverage: Math.floor(10000 / d.min_initial_margin_fraction),
229
+ marginTiers: null,
230
+ session: null, // RH: 24/7, no session info published
231
+ tradable: d.status === "active" && d.is_frozen !== true,
232
+ };
233
+ }
234
+ export function lighterPublicData(venue) {
235
+ return {
236
+ async fetchMarkets(net) {
237
+ const [details, funding] = await Promise.all([fetchPerpDetails(venue, net), fetchOwnFunding(venue, net)]);
238
+ const out = [];
239
+ const unclassified = [];
240
+ for (const d of details) {
241
+ const cls = classify(venue, d.symbol);
242
+ if (cls === null) {
243
+ unclassified.push(d.symbol);
244
+ continue;
245
+ }
246
+ out.push(toMarket(venue, net, d, cls, funding.get(d.market_id)));
247
+ }
248
+ warnExcluded(venue, "unclassified/ambiguous market(s)", unclassified);
249
+ return out;
250
+ },
251
+ async fetchFundingRates(net) {
252
+ const funding = await fetchOwnFunding(venue, net);
253
+ const out = [];
254
+ const unclassified = [];
255
+ for (const r of funding.values()) {
256
+ const cls = classify(venue, r.symbol);
257
+ if (cls === null) {
258
+ unclassified.push(r.symbol);
259
+ continue;
260
+ }
261
+ out.push({
262
+ venue,
263
+ venueSymbol: String(r.market_id),
264
+ symbol: r.symbol,
265
+ canonical: canonicalFor(venue, r.symbol, cls),
266
+ rate: venueRateField(r.rate, "funding-rates rate", venue),
267
+ basis: BASIS_8H,
268
+ markPrice: null, // /funding-rates carries no mark price
269
+ nextFundingTime: null, // not published
270
+ });
271
+ }
272
+ warnExcluded(venue, "unclassified/ambiguous market(s)", unclassified);
273
+ return out;
274
+ },
275
+ };
276
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Lighter RH deposit on Robinhood Chain (apidocs.rh.lighter.xyz
3
+ * "Deposits, Transfers, and Withdrawals" + apidocs.lighter.xyz/docs/lighter-rh):
4
+ *
5
+ * ERC-20 approve(ZkLighterContract, amount) → deposit(_to, _assetIndex, _routeType, _amount)
6
+ *
7
+ * Contract, chain id and token are PINNED in the venue definition (networks.<net>.depositPins,
8
+ * verified against the official docs) and cross-checked against the live venue API — any
9
+ * disagreement is EXCHANGE_ERROR before anything is signed (a compromised/misconfigured API
10
+ * cannot redirect the approve or the deposit):
11
+ * - contract — GET /api/v1/layer1BasicInfo contract_addresses[name=ZkLighterContract] == pins.contract
12
+ * - L1 chain — GET /api/v1/layer1BasicInfo l1_providers[0].chainId == pins.chainId == RPC eth_chainId
13
+ * - token — GET /api/v1/assetDetails?asset_id=<collateralAssetIndex> l1_address == pins.token,
14
+ * symbol must equal the venue collateral (USDG); l1_decimals read live
15
+ * - RPC — venue definition networks.<net>.rpcUrl
16
+ * Minimum 1 USDG per deposit (docs). Only USDG may be routed to perps (route 0).
17
+ *
18
+ * Testnet: the RH testnet L1 (chainId 123456) has no documented RPC → CAPABILITY_UNSUPPORTED.
19
+ * Transactions are signed by the master wallet through a transaction signer (the digest is
20
+ * derived from the unsigned tx inside the signer — no raw-digest API).
21
+ */
22
+ import { Transaction } from "ethers";
23
+ import type { Network } from "./venue-types.js";
24
+ /** Docs: "There is a minimum of 1 USDG, and equivalent for other ERC20s, per deposit." */
25
+ export declare const LIGHTER_RH_MIN_DEPOSIT = 1;
26
+ export interface LighterRhDepositPlan {
27
+ network: Network;
28
+ rpcUrl: string;
29
+ chainId: number;
30
+ contract: string;
31
+ asset: string;
32
+ assetIndex: number;
33
+ token: string;
34
+ decimals: number;
35
+ amount: string;
36
+ amountRaw: bigint;
37
+ routeType: 0 | 1;
38
+ depositor: string;
39
+ tokenBalance: bigint;
40
+ allowance: bigint;
41
+ needsApprove: boolean;
42
+ }
43
+ export interface LighterRhDepositResult {
44
+ approveTxHash: string | null;
45
+ depositTxHash: string;
46
+ }
47
+ /** Signs an unsigned tx (hashing internally) and returns the 65-byte 0x signature. */
48
+ export type TxSigner = (tx: Transaction) => Promise<string>;
49
+ /** Deposit is offered on mainnet only; testnet → uniform CAPABILITY_UNSUPPORTED. */
50
+ export declare function requireLighterRhDepositNetwork(net: Network): void;
51
+ /**
52
+ * Read-only: resolve contract / token / chain from the venue API and the depositor's
53
+ * balance + allowance from the RPC. Throws before anything is signed.
54
+ */
55
+ export declare function planLighterRhDeposit(p: {
56
+ network: Network;
57
+ amount: string;
58
+ depositor: string;
59
+ routeType: 0 | 1;
60
+ }): Promise<LighterRhDepositPlan>;
61
+ /** Sends approve (only when the allowance is short — exact amount, not unlimited) then deposit. */
62
+ export declare function executeLighterRhDeposit(plan: LighterRhDepositPlan, sign: TxSigner): Promise<LighterRhDepositResult>;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Lighter RH deposit on Robinhood Chain (apidocs.rh.lighter.xyz
3
+ * "Deposits, Transfers, and Withdrawals" + apidocs.lighter.xyz/docs/lighter-rh):
4
+ *
5
+ * ERC-20 approve(ZkLighterContract, amount) → deposit(_to, _assetIndex, _routeType, _amount)
6
+ *
7
+ * Contract, chain id and token are PINNED in the venue definition (networks.<net>.depositPins,
8
+ * verified against the official docs) and cross-checked against the live venue API — any
9
+ * disagreement is EXCHANGE_ERROR before anything is signed (a compromised/misconfigured API
10
+ * cannot redirect the approve or the deposit):
11
+ * - contract — GET /api/v1/layer1BasicInfo contract_addresses[name=ZkLighterContract] == pins.contract
12
+ * - L1 chain — GET /api/v1/layer1BasicInfo l1_providers[0].chainId == pins.chainId == RPC eth_chainId
13
+ * - token — GET /api/v1/assetDetails?asset_id=<collateralAssetIndex> l1_address == pins.token,
14
+ * symbol must equal the venue collateral (USDG); l1_decimals read live
15
+ * - RPC — venue definition networks.<net>.rpcUrl
16
+ * Minimum 1 USDG per deposit (docs). Only USDG may be routed to perps (route 0).
17
+ *
18
+ * Testnet: the RH testnet L1 (chainId 123456) has no documented RPC → CAPABILITY_UNSUPPORTED.
19
+ * Transactions are signed by the master wallet through a transaction signer (the digest is
20
+ * derived from the unsigned tx inside the signer — no raw-digest API).
21
+ */
22
+ import { Interface, JsonRpcProvider, Network as EthersNetwork, parseUnits } from "ethers";
23
+ import { sendMasterTx } from "../evm-master-tx.js";
24
+ import { PerpError } from "../errors.js";
25
+ import { unsupported } from "./trading-v2.js";
26
+ import { LIGHTER_RH_NETWORKS } from "./venues/lighter-family.js";
27
+ /** Docs: "There is a minimum of 1 USDG, and equivalent for other ERC20s, per deposit." */
28
+ export const LIGHTER_RH_MIN_DEPOSIT = 1;
29
+ const erc20 = new Interface([
30
+ "function balanceOf(address) view returns (uint256)",
31
+ "function allowance(address owner, address spender) view returns (uint256)",
32
+ "function approve(address spender, uint256 amount) returns (bool)",
33
+ ]);
34
+ const zkLighter = new Interface([
35
+ "function deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount) payable",
36
+ ]);
37
+ async function getJson(url) {
38
+ const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
39
+ const text = await res.text();
40
+ if (!res.ok)
41
+ throw new PerpError("EXCHANGE_ERROR", `GET ${url} HTTP ${res.status}`, { exchange: "lighter-rh", body: text.slice(0, 300) });
42
+ try {
43
+ return JSON.parse(text);
44
+ }
45
+ catch {
46
+ throw new PerpError("EXCHANGE_ERROR", `GET ${url}: invalid JSON`, { exchange: "lighter-rh", body: text.slice(0, 300) });
47
+ }
48
+ }
49
+ function venueError(msg) {
50
+ return new PerpError("EXCHANGE_ERROR", `lighter-rh: ${msg}`, { exchange: "lighter-rh" });
51
+ }
52
+ function provider(rpcUrl, chainId) {
53
+ const net = EthersNetwork.from(chainId);
54
+ return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
55
+ }
56
+ /** Deposit is offered on mainnet only; testnet → uniform CAPABILITY_UNSUPPORTED. */
57
+ export function requireLighterRhDepositNetwork(net) {
58
+ if (net === "testnet") {
59
+ throw unsupported("Lighter RH", "deposit", "the RH testnet L1 (chainId 123456) has no documented RPC; testnet collateral comes from the faucet", "Use the faucet in the Lighter RH testnet app");
60
+ }
61
+ }
62
+ /**
63
+ * Read-only: resolve contract / token / chain from the venue API and the depositor's
64
+ * balance + allowance from the RPC. Throws before anything is signed.
65
+ */
66
+ export async function planLighterRhDeposit(p) {
67
+ requireLighterRhDepositNetwork(p.network);
68
+ const cfg = LIGHTER_RH_NETWORKS[p.network];
69
+ if (cfg.rpcUrl === null)
70
+ throw new Error(`lighter-rh ${p.network} venue definition is missing rpcUrl`);
71
+ const assetIndex = cfg.collateralAssetIndex;
72
+ const asset = cfg.collateral[0];
73
+ if (assetIndex === null || asset === undefined)
74
+ throw new Error(`lighter-rh ${p.network} venue definition has no collateral asset`);
75
+ const pins = cfg.depositPins;
76
+ if (pins === null) {
77
+ throw new PerpError("NOT_IMPLEMENTED", `Lighter RH ${p.network} deposit has no pinned contract/token addresses`, {
78
+ remediation: "Deposit from the Lighter RH web app; the CLI only deposits to officially verified, pinned addresses",
79
+ });
80
+ }
81
+ if (!/^0x[0-9a-fA-F]{40}$/.test(p.depositor)) {
82
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Lighter RH deposit needs the master wallet address", {
83
+ remediation: "Run with the master wallet: perp -e lighter-rh --ows <wallet> funds deposit lighter-rh <amount>",
84
+ });
85
+ }
86
+ if (!/^\d+(\.\d+)?$/.test(p.amount) || !(Number(p.amount) > 0)) {
87
+ throw new PerpError("INVALID_PARAMS", `Deposit amount must be a positive decimal (got "${p.amount}")`);
88
+ }
89
+ if (Number(p.amount) < LIGHTER_RH_MIN_DEPOSIT) {
90
+ throw new PerpError("SIZE_TOO_SMALL", `Lighter RH minimum deposit is ${LIGHTER_RH_MIN_DEPOSIT} ${asset} (got ${p.amount})`);
91
+ }
92
+ const base = `${cfg.restUrl}/api/v1`;
93
+ const [l1, details] = await Promise.all([
94
+ getJson(`${base}/layer1BasicInfo`),
95
+ getJson(`${base}/assetDetails?asset_id=${assetIndex}`),
96
+ ]);
97
+ const contracts = l1.contract_addresses;
98
+ if (!Array.isArray(contracts))
99
+ throw venueError("layer1BasicInfo: missing contract_addresses");
100
+ const zk = contracts.find(c => c.name === "ZkLighterContract");
101
+ if (zk === undefined || typeof zk.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(zk.address)) {
102
+ throw venueError("layer1BasicInfo: no ZkLighterContract address");
103
+ }
104
+ const providers = l1.l1_providers;
105
+ const l1ChainId = Array.isArray(providers) ? providers[0]?.chainId : undefined;
106
+ if (typeof l1ChainId !== "number" || !Number.isInteger(l1ChainId))
107
+ throw venueError("layer1BasicInfo: missing l1_providers[0].chainId");
108
+ const list = details.asset_details;
109
+ if (!Array.isArray(list))
110
+ throw venueError("assetDetails: missing asset_details");
111
+ const a = list.find(x => x.asset_id === assetIndex);
112
+ if (a === undefined)
113
+ throw venueError(`assetDetails: asset ${assetIndex} not listed`);
114
+ if (a.symbol !== asset)
115
+ throw venueError(`assetDetails: asset ${assetIndex} is ${String(a.symbol)}, expected ${asset}`);
116
+ if (typeof a.l1_address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(a.l1_address))
117
+ throw venueError(`assetDetails: ${asset} has no l1_address`);
118
+ if (typeof a.l1_decimals !== "number" || !Number.isInteger(a.l1_decimals))
119
+ throw venueError(`assetDetails: ${asset} has no l1_decimals`);
120
+ const same = (x, y) => x.toLowerCase() === y.toLowerCase();
121
+ if (!same(zk.address, pins.contract)) {
122
+ throw venueError(`layer1BasicInfo ZkLighterContract ${zk.address} != pinned ${pins.contract} — refusing to approve/deposit (verify at ${pins.sources[0]})`);
123
+ }
124
+ if (l1ChainId !== pins.chainId) {
125
+ throw venueError(`layer1BasicInfo chainId ${l1ChainId} != pinned ${pins.chainId} — refusing to deposit`);
126
+ }
127
+ if (!same(a.l1_address, pins.token)) {
128
+ throw venueError(`assetDetails ${asset} l1_address ${a.l1_address} != pinned ${pins.token} — refusing to approve/deposit`);
129
+ }
130
+ const rpc = provider(cfg.rpcUrl, l1ChainId);
131
+ const rpcChain = BigInt(await rpc.send("eth_chainId", []));
132
+ if (rpcChain !== BigInt(l1ChainId)) {
133
+ throw venueError(`RPC ${cfg.rpcUrl} serves chainId ${rpcChain}, venue L1 is ${l1ChainId}`);
134
+ }
135
+ let amountRaw;
136
+ try {
137
+ amountRaw = parseUnits(p.amount, a.l1_decimals);
138
+ }
139
+ catch (e) {
140
+ throw new PerpError("INVALID_PARAMS", `Lighter RH deposit amount "${p.amount}" is not a decimal ${asset} amount with at most ${a.l1_decimals} decimals: ${e instanceof Error ? e.message : String(e)}`, {
141
+ exchange: "lighter-rh", remediation: `Pass a plain decimal amount with at most ${a.l1_decimals} decimals, e.g. 10 or 10.5`,
142
+ });
143
+ }
144
+ if (amountRaw <= 0n) {
145
+ throw new PerpError("INVALID_PARAMS", `Lighter RH deposit amount must be positive (got "${p.amount}")`, { exchange: "lighter-rh" });
146
+ }
147
+ const [balRaw, allowRaw] = await Promise.all([
148
+ rpc.call({ to: a.l1_address, data: erc20.encodeFunctionData("balanceOf", [p.depositor]) }),
149
+ rpc.call({ to: a.l1_address, data: erc20.encodeFunctionData("allowance", [p.depositor, zk.address]) }),
150
+ ]);
151
+ const tokenBalance = erc20.decodeFunctionResult("balanceOf", balRaw)[0];
152
+ const allowance = erc20.decodeFunctionResult("allowance", allowRaw)[0];
153
+ if (tokenBalance < amountRaw) {
154
+ throw new PerpError("INSUFFICIENT_BALANCE", `Wallet ${p.depositor} holds ${tokenBalance} raw ${asset} on Robinhood Chain; deposit needs ${amountRaw}`, {
155
+ exchange: "lighter-rh", remediation: `Fund ${p.depositor} with ${asset} on Robinhood Chain (chainId ${l1ChainId}) first`,
156
+ });
157
+ }
158
+ return {
159
+ network: p.network, rpcUrl: cfg.rpcUrl, chainId: pins.chainId, contract: pins.contract,
160
+ asset, assetIndex, token: pins.token, decimals: a.l1_decimals,
161
+ amount: p.amount, amountRaw, routeType: p.routeType, depositor: p.depositor,
162
+ tokenBalance, allowance, needsApprove: allowance < amountRaw,
163
+ };
164
+ }
165
+ /**
166
+ * Simulate, sign, broadcast one tx and wait for a successful receipt (shared master-tx path:
167
+ * once broadcast, a lost response / receipt timeout is ORDER_PENDING carrying the tx hash).
168
+ */
169
+ async function sendTx(rpc, plan, to, data, label, sign) {
170
+ const { txHash } = await sendMasterTx(rpc, {
171
+ venue: "lighter-rh", chainLabel: "Robinhood Chain", chainId: plan.chainId, from: plan.depositor, to, data, value: 0n, label,
172
+ }, sign);
173
+ return txHash;
174
+ }
175
+ /** Sends approve (only when the allowance is short — exact amount, not unlimited) then deposit. */
176
+ export async function executeLighterRhDeposit(plan, sign) {
177
+ const rpc = provider(plan.rpcUrl, plan.chainId);
178
+ let approveTxHash = null;
179
+ if (plan.needsApprove) {
180
+ approveTxHash = await sendTx(rpc, plan, plan.token, erc20.encodeFunctionData("approve", [plan.contract, plan.amountRaw]), `${plan.asset} approve`, sign);
181
+ }
182
+ const data = zkLighter.encodeFunctionData("deposit", [plan.depositor, plan.assetIndex, plan.routeType, plan.amountRaw]);
183
+ const depositTxHash = await sendTx(rpc, plan, plan.contract, data, "Lighter RH deposit", sign);
184
+ return { approveTxHash, depositTxHash };
185
+ }