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.
- package/README.md +278 -31
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +74 -84
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +33 -16
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- 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
|
+
}
|