perp-cli 0.12.17 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- 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 +303 -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 +70 -92
- 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 +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- 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 +357 -459
- 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 +240 -6
- package/dist/exchanges/aster.js +1139 -195
- 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 +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
- package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- 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 +1386 -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 +29 -0
- package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
- package/dist/exchanges/venues/qfex.js +140 -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 +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -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 +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- 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 +31 -15
- 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,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperliquid Outcome Adapter (HIP-4).
|
|
3
|
+
*
|
|
4
|
+
* Composes with HyperliquidAdapter for signing. Asset id encoding:
|
|
5
|
+
* 100,000,000 + (10 * outcome + side)
|
|
6
|
+
*
|
|
7
|
+
* Coin name conventions:
|
|
8
|
+
* - l2Book/candle/allMids: `#<encoding>` (e.g., `#10`)
|
|
9
|
+
* - spotClearinghouseState balance: `+<encoding>` (e.g., `+10`)
|
|
10
|
+
*
|
|
11
|
+
* Outcome trades draw collateral from the spot balance of the outcome's own quote token —
|
|
12
|
+
* `outcomeMeta.outcomes[].quoteToken` (live 2026-10-12: "USDC" on every listed outcome; a USDH-only
|
|
13
|
+
* balance was refused by the venue "Insufficient spot balance asset=100014881"). Min order value:
|
|
14
|
+
* 10 quote tokens (docs error-responses MinTradeSpotNtl "Order must have minimum value of 10 {quote_token}.").
|
|
15
|
+
*/
|
|
16
|
+
import { PerpError } from "../errors.js";
|
|
17
|
+
const OUTCOME_ASSET_OFFSET = 100_000_000;
|
|
18
|
+
/** docs error-responses MinTradeSpotNtl: "Order must have minimum value of 10 {quote_token}." */
|
|
19
|
+
const MIN_ORDER_QUOTE = 10;
|
|
20
|
+
/** Outcome encoding `10*outcome + side` keeps side as the units digit, so
|
|
21
|
+
* side must be 0..9 to avoid overflow into the next outcome's id space. */
|
|
22
|
+
const MAX_SIDE = 9;
|
|
23
|
+
/** Asset id offset spans `[100_000_000, 200_000_000)`, so encoding < 100M. */
|
|
24
|
+
const MAX_ENCODING = 99_999_999;
|
|
25
|
+
export class HyperliquidOutcomeAdapter {
|
|
26
|
+
name = "hyperliquid";
|
|
27
|
+
_hl;
|
|
28
|
+
_outcomeMeta = null;
|
|
29
|
+
_initialized = false;
|
|
30
|
+
constructor(hlAdapter) {
|
|
31
|
+
this._hl = hlAdapter;
|
|
32
|
+
}
|
|
33
|
+
async init() {
|
|
34
|
+
if (this._initialized)
|
|
35
|
+
return;
|
|
36
|
+
await this._loadOutcomeMeta();
|
|
37
|
+
this._initialized = true;
|
|
38
|
+
}
|
|
39
|
+
static encoding(outcome, side) {
|
|
40
|
+
return 10 * outcome + side;
|
|
41
|
+
}
|
|
42
|
+
static assetId(outcome, side) {
|
|
43
|
+
return OUTCOME_ASSET_OFFSET + HyperliquidOutcomeAdapter.encoding(outcome, side);
|
|
44
|
+
}
|
|
45
|
+
static mintCoin(outcome, side) {
|
|
46
|
+
return `#${HyperliquidOutcomeAdapter.encoding(outcome, side)}`;
|
|
47
|
+
}
|
|
48
|
+
static balanceCoin(outcome, side) {
|
|
49
|
+
return `+${HyperliquidOutcomeAdapter.encoding(outcome, side)}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decode a balance coin name like `+10` back to (outcome, side).
|
|
53
|
+
* Returns null if the name does not match the outcome scheme.
|
|
54
|
+
*/
|
|
55
|
+
static decodeBalanceCoin(coin) {
|
|
56
|
+
if (!coin.startsWith("+"))
|
|
57
|
+
return null;
|
|
58
|
+
const enc = Number(coin.slice(1));
|
|
59
|
+
if (!Number.isFinite(enc) || enc < 0)
|
|
60
|
+
return null;
|
|
61
|
+
return { outcome: Math.floor(enc / 10), side: enc % 10 };
|
|
62
|
+
}
|
|
63
|
+
static parseDescription(description) {
|
|
64
|
+
const parts = description.split("|");
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const p of parts) {
|
|
67
|
+
const idx = p.indexOf(":");
|
|
68
|
+
if (idx < 0)
|
|
69
|
+
continue;
|
|
70
|
+
const key = p.slice(0, idx).trim();
|
|
71
|
+
const val = p.slice(idx + 1).trim();
|
|
72
|
+
if (key === "class")
|
|
73
|
+
out.class = val;
|
|
74
|
+
else if (key === "underlying")
|
|
75
|
+
out.underlying = val;
|
|
76
|
+
else if (key === "expiry")
|
|
77
|
+
out.expiryMs = HyperliquidOutcomeAdapter._parseExpiry(val);
|
|
78
|
+
else if (key === "targetPrice")
|
|
79
|
+
out.targetPrice = Number(val);
|
|
80
|
+
else if (key === "period")
|
|
81
|
+
out.period = val;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Compute the underlying mark-price view (gap / inTheMoney) for an outcome
|
|
87
|
+
* from a parsed description and the live allMids map.
|
|
88
|
+
*
|
|
89
|
+
* Pure helper — extracted from getView so the settlement-status logic is
|
|
90
|
+
* directly unit-testable.
|
|
91
|
+
*
|
|
92
|
+
* - HL `allMids` keys perps by bare symbol (e.g. "BTC"). HIP-3 perps use
|
|
93
|
+
* "@dexIdx:SYMBOL" but those aren't referenced in HIP-4 outcomes yet.
|
|
94
|
+
* - For `class:priceBinary` the convention is Yes = "underlying >= target".
|
|
95
|
+
* When `class` is missing or non-binary, `inTheMoney` stays null rather
|
|
96
|
+
* than guessing (Rule #2 — no silent classification fallback).
|
|
97
|
+
* - Returns null when there is no underlying field to look up.
|
|
98
|
+
*/
|
|
99
|
+
static _computeUnderlying(parsed, allMids) {
|
|
100
|
+
if (!parsed.underlying)
|
|
101
|
+
return null;
|
|
102
|
+
const sym = parsed.underlying.toUpperCase();
|
|
103
|
+
const markPrice = allMids[sym];
|
|
104
|
+
const target = parsed.targetPrice;
|
|
105
|
+
let gap;
|
|
106
|
+
let gapPct;
|
|
107
|
+
let inTheMoney = null;
|
|
108
|
+
if (markPrice !== undefined && target !== undefined) {
|
|
109
|
+
gap = Number(markPrice) - target;
|
|
110
|
+
gapPct = (gap / target) * 100;
|
|
111
|
+
if (parsed.class === "priceBinary" && Number.isFinite(gap)) {
|
|
112
|
+
inTheMoney = gap >= 0 ? "yes" : "no";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
symbol: sym,
|
|
117
|
+
source: sym,
|
|
118
|
+
markPrice,
|
|
119
|
+
targetPrice: target,
|
|
120
|
+
gap,
|
|
121
|
+
gapPct,
|
|
122
|
+
inTheMoney,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Sum of `impliedProb` across sides — for fair binary markets the sum
|
|
127
|
+
* should converge to ~1.0. Deviation hints at arbitrage or stale mids.
|
|
128
|
+
*
|
|
129
|
+
* Returns undefined when any side is missing impliedProb OR when any
|
|
130
|
+
* impliedProb is non-finite (NaN, Infinity). This means "we don't have a
|
|
131
|
+
* trustworthy view of the symmetry right now" rather than emitting NaN
|
|
132
|
+
* downstream (Rule #2 — no silent garbage propagation).
|
|
133
|
+
*/
|
|
134
|
+
static _computeMidSum(sides) {
|
|
135
|
+
if (sides.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
for (const s of sides) {
|
|
138
|
+
if (s.impliedProb === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (!Number.isFinite(s.impliedProb))
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return sides.reduce((acc, s) => acc + (s.impliedProb ?? 0), 0);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Compute the time-status pair (`serverTime`, `msToExpiry`) for a view.
|
|
147
|
+
* Pure helper — takes `nowMs` as an argument so callers can inject a
|
|
148
|
+
* deterministic clock under test.
|
|
149
|
+
*
|
|
150
|
+
* `msToExpiry` is the raw signed delta `expiryMs - nowMs`:
|
|
151
|
+
* positive = unexpired
|
|
152
|
+
* zero = at expiry
|
|
153
|
+
* negative = already settled (caller decides UX)
|
|
154
|
+
* undefined = unknown expiry
|
|
155
|
+
*
|
|
156
|
+
* Does NOT clamp negatives or treat them as "expired" — that
|
|
157
|
+
* classification is the caller's job (Rule #2 — no silent classification
|
|
158
|
+
* fallback in a low-level helper).
|
|
159
|
+
*/
|
|
160
|
+
static _computeTimeStatus(expiryMs, nowMs) {
|
|
161
|
+
return {
|
|
162
|
+
serverTime: nowMs,
|
|
163
|
+
msToExpiry: expiryMs !== undefined ? expiryMs - nowMs : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Pure arithmetic gate for the (outcome, side) pair.
|
|
168
|
+
*
|
|
169
|
+
* Rejects NaN / non-integer / negative values immediately so the
|
|
170
|
+
* encoding formula `10 * outcome + side` never produces a garbage
|
|
171
|
+
* asset id silently. Does NOT consult outcomeMeta — that lookup is in
|
|
172
|
+
* the instance-level `_validateOutcomeSide` which composes this
|
|
173
|
+
* helper with the live registry check.
|
|
174
|
+
*
|
|
175
|
+
* Boundary: outcome=9_999_999, side=9 → encoding=99_999_999 = MAX_ENCODING (valid).
|
|
176
|
+
* outcome=10_000_000, side=0 → encoding=100_000_000 > MAX_ENCODING (rejected).
|
|
177
|
+
*/
|
|
178
|
+
static _assertOutcomeRange(outcome, side) {
|
|
179
|
+
if (!Number.isInteger(outcome) || outcome < 0) {
|
|
180
|
+
throw new PerpError("INVALID_PARAMS", `Outcome id must be a non-negative integer, got: ${outcome}`, { exchange: "hyperliquid" });
|
|
181
|
+
}
|
|
182
|
+
if (!Number.isInteger(side) || side < 0 || side > MAX_SIDE) {
|
|
183
|
+
throw new PerpError("INVALID_PARAMS", `Side must be an integer 0..${MAX_SIDE} (encoding scheme is single digit), got: ${side}`, { exchange: "hyperliquid" });
|
|
184
|
+
}
|
|
185
|
+
const encoding = HyperliquidOutcomeAdapter.encoding(outcome, side);
|
|
186
|
+
if (encoding > MAX_ENCODING) {
|
|
187
|
+
throw new PerpError("INVALID_PARAMS", `Encoding ${encoding} overflows the outcome asset block (max ${MAX_ENCODING})`, { exchange: "hyperliquid" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Trim a raw orderbook to `depth` levels and surface best bid/ask.
|
|
192
|
+
*
|
|
193
|
+
* Throws (rather than silently coercing) when:
|
|
194
|
+
* - `book.bids` or `book.asks` is missing/non-array (venue payload
|
|
195
|
+
* malformed — Rule #2: don't fabricate an empty book)
|
|
196
|
+
* - `depth` is not a non-negative integer (NaN, negative, Infinity,
|
|
197
|
+
* fractional are all caller bugs that previously silently produced
|
|
198
|
+
* `slice(0, NaN) === []` or `slice(0, -1)` = "all but last")
|
|
199
|
+
*/
|
|
200
|
+
static _trimBook(book, depth) {
|
|
201
|
+
if (!book || !Array.isArray(book.bids) || !Array.isArray(book.asks)) {
|
|
202
|
+
throw new PerpError("EXCHANGE_ERROR", "Outcome orderbook response is missing bids/asks array", { exchange: "hyperliquid" });
|
|
203
|
+
}
|
|
204
|
+
if (!Number.isInteger(depth) || depth < 0) {
|
|
205
|
+
throw new PerpError("INVALID_PARAMS", `Depth must be a non-negative integer, got: ${depth}`, { exchange: "hyperliquid" });
|
|
206
|
+
}
|
|
207
|
+
const bids = book.bids.slice(0, depth);
|
|
208
|
+
const asks = book.asks.slice(0, depth);
|
|
209
|
+
return {
|
|
210
|
+
bids,
|
|
211
|
+
asks,
|
|
212
|
+
bestBid: bids[0]?.[0],
|
|
213
|
+
bestAsk: asks[0]?.[0],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** Parse "20260504-0600" → ms-epoch (UTC). Returns undefined for malformed. */
|
|
217
|
+
static _parseExpiry(s) {
|
|
218
|
+
const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(s);
|
|
219
|
+
if (!m)
|
|
220
|
+
return undefined;
|
|
221
|
+
const [, y, mo, d, h, mi] = m;
|
|
222
|
+
const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi));
|
|
223
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
224
|
+
}
|
|
225
|
+
async _loadOutcomeMeta() {
|
|
226
|
+
const meta = await this._infoPost({ type: "outcomeMeta" });
|
|
227
|
+
if (!meta || typeof meta !== "object" || !("outcomes" in meta)) {
|
|
228
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid outcomeMeta returned unexpected shape: ${JSON.stringify(meta).slice(0, 200)}`, { exchange: "hyperliquid" });
|
|
229
|
+
}
|
|
230
|
+
this._outcomeMeta = meta;
|
|
231
|
+
}
|
|
232
|
+
async getMarkets() {
|
|
233
|
+
await this.init();
|
|
234
|
+
if (!this._outcomeMeta)
|
|
235
|
+
return [];
|
|
236
|
+
const allMids = await this._infoPost({ type: "allMids" });
|
|
237
|
+
return this._outcomeMeta.outcomes.map((o) => {
|
|
238
|
+
const parsed = HyperliquidOutcomeAdapter.parseDescription(o.description);
|
|
239
|
+
const sides = o.sideSpecs.map((s, i) => {
|
|
240
|
+
const encoding = HyperliquidOutcomeAdapter.encoding(o.outcome, i);
|
|
241
|
+
return {
|
|
242
|
+
side: i,
|
|
243
|
+
name: s.name,
|
|
244
|
+
encoding,
|
|
245
|
+
assetId: OUTCOME_ASSET_OFFSET + encoding,
|
|
246
|
+
mid: allMids[`#${encoding}`],
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
outcome: o.outcome,
|
|
251
|
+
quoteToken: this.quoteTokenOf(o.outcome),
|
|
252
|
+
name: o.name,
|
|
253
|
+
description: o.description,
|
|
254
|
+
...parsed,
|
|
255
|
+
sides,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async getPositions() {
|
|
260
|
+
await this.init();
|
|
261
|
+
// Bypass HL adapter's cached spot state — outcome positions change at
|
|
262
|
+
// every fill, and the spot cache TTL would surface stale data.
|
|
263
|
+
const userAddress = await this._resolveUserAddress();
|
|
264
|
+
const state = await this._infoPost({ type: "spotClearinghouseState", user: userAddress });
|
|
265
|
+
const balances = (state?.balances ?? []);
|
|
266
|
+
const allMidsPromise = this._infoPost({ type: "allMids" });
|
|
267
|
+
const positions = [];
|
|
268
|
+
for (const b of balances) {
|
|
269
|
+
const coin = String(b.coin ?? "");
|
|
270
|
+
const decoded = HyperliquidOutcomeAdapter.decodeBalanceCoin(coin);
|
|
271
|
+
if (!decoded)
|
|
272
|
+
continue;
|
|
273
|
+
const total = String(b.total ?? "0");
|
|
274
|
+
if (Number(total) === 0)
|
|
275
|
+
continue;
|
|
276
|
+
const sideName = this._lookupSideName(decoded.outcome, decoded.side);
|
|
277
|
+
positions.push({
|
|
278
|
+
outcome: decoded.outcome,
|
|
279
|
+
side: decoded.side,
|
|
280
|
+
sideName,
|
|
281
|
+
encoding: HyperliquidOutcomeAdapter.encoding(decoded.outcome, decoded.side),
|
|
282
|
+
size: total,
|
|
283
|
+
entryNotional: String(b.entryNtl ?? "0"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (positions.length === 0)
|
|
287
|
+
return [];
|
|
288
|
+
const allMids = await allMidsPromise;
|
|
289
|
+
for (const p of positions) {
|
|
290
|
+
const mark = allMids[`#${p.encoding}`];
|
|
291
|
+
if (mark) {
|
|
292
|
+
p.markPrice = mark;
|
|
293
|
+
p.unrealizedPnl = String(Number(mark) * Number(p.size) - Number(p.entryNotional));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return positions;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Assemble a combined view of one outcome: all sides' books in parallel,
|
|
300
|
+
* the underlying mark price (HL perp mid for `description.underlying`),
|
|
301
|
+
* gap vs targetPrice, time-to-expiry, and per-side implied probability.
|
|
302
|
+
*
|
|
303
|
+
* Outcome markets have a symmetric (Yes / No) structure where the prices
|
|
304
|
+
* sum to ~$1; this view exposes both sides in a single round-trip and
|
|
305
|
+
* also surfaces the directional context for binary markets — what BTC
|
|
306
|
+
* mark price would settle the contract right now.
|
|
307
|
+
*/
|
|
308
|
+
async getView(outcome, depth = 10) {
|
|
309
|
+
await this.init();
|
|
310
|
+
const meta = this._outcomeMeta?.outcomes.find((o) => o.outcome === outcome);
|
|
311
|
+
if (!meta) {
|
|
312
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, {
|
|
313
|
+
exchange: "hyperliquid",
|
|
314
|
+
remediation: "Run: perp outcome list",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const parsed = HyperliquidOutcomeAdapter.parseDescription(meta.description);
|
|
318
|
+
// Fetch books for every side in parallel + allMids once for mids and
|
|
319
|
+
// the underlying symbol's mark price.
|
|
320
|
+
const allMidsPromise = this._infoPost({ type: "allMids" });
|
|
321
|
+
const bookPromises = meta.sideSpecs.map((_, i) => this.getOrderbook(outcome, i));
|
|
322
|
+
const [allMids, ...books] = await Promise.all([allMidsPromise, ...bookPromises]);
|
|
323
|
+
// Trim each book to `depth` levels and compute best bid/ask + implied prob.
|
|
324
|
+
const sides = meta.sideSpecs.map((spec, i) => {
|
|
325
|
+
const encoding = HyperliquidOutcomeAdapter.encoding(outcome, i);
|
|
326
|
+
const trimmed = HyperliquidOutcomeAdapter._trimBook(books[i], depth);
|
|
327
|
+
const mid = allMids[`#${encoding}`];
|
|
328
|
+
return {
|
|
329
|
+
side: i,
|
|
330
|
+
name: spec.name,
|
|
331
|
+
encoding,
|
|
332
|
+
assetId: OUTCOME_ASSET_OFFSET + encoding,
|
|
333
|
+
mid,
|
|
334
|
+
bids: trimmed.bids,
|
|
335
|
+
asks: trimmed.asks,
|
|
336
|
+
bestBid: trimmed.bestBid,
|
|
337
|
+
bestAsk: trimmed.bestAsk,
|
|
338
|
+
impliedProb: mid !== undefined ? Number(mid) : undefined,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const midSum = HyperliquidOutcomeAdapter._computeMidSum(sides);
|
|
342
|
+
// Underlying: HL perp mid for the parsed underlying symbol.
|
|
343
|
+
const underlying = HyperliquidOutcomeAdapter._computeUnderlying(parsed, allMids);
|
|
344
|
+
const { serverTime, msToExpiry } = HyperliquidOutcomeAdapter._computeTimeStatus(parsed.expiryMs, Date.now());
|
|
345
|
+
return {
|
|
346
|
+
outcome,
|
|
347
|
+
name: meta.name,
|
|
348
|
+
description: meta.description,
|
|
349
|
+
class: parsed.class,
|
|
350
|
+
expiryMs: parsed.expiryMs,
|
|
351
|
+
msToExpiry,
|
|
352
|
+
period: parsed.period,
|
|
353
|
+
underlying,
|
|
354
|
+
sides,
|
|
355
|
+
midSum,
|
|
356
|
+
serverTime,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async getOrderbook(outcome, side) {
|
|
360
|
+
await this.init();
|
|
361
|
+
this._validateOutcomeSide(outcome, side);
|
|
362
|
+
const coin = HyperliquidOutcomeAdapter.mintCoin(outcome, side);
|
|
363
|
+
const book = await this._infoPost({ type: "l2Book", coin });
|
|
364
|
+
// Rule #2: do NOT fabricate an empty book when the venue payload is
|
|
365
|
+
// malformed. Caller (typically getView) has its own gates downstream
|
|
366
|
+
// but a missing `levels` here is a venue contract break, not "no
|
|
367
|
+
// resting orders".
|
|
368
|
+
if (!book ||
|
|
369
|
+
!Array.isArray(book.levels) ||
|
|
370
|
+
!Array.isArray(book.levels[0]) ||
|
|
371
|
+
!Array.isArray(book.levels[1])) {
|
|
372
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned malformed payload for ${coin}: missing or non-array \`levels\``, { exchange: "hyperliquid" });
|
|
373
|
+
}
|
|
374
|
+
const levels = book.levels;
|
|
375
|
+
// Rule #2: `Number("abc") ?? 0` would silently coerce a non-numeric
|
|
376
|
+
// venue time to 0 (1970 epoch). 0 is a legitimate "no time given"
|
|
377
|
+
// value when book.time is undefined, but a NaN should not be hidden.
|
|
378
|
+
//
|
|
379
|
+
// Strict policy (qa/2026-05-16): empty string is corruption too —
|
|
380
|
+
// `Number("") === 0` would land in the same fake-1970-epoch bucket
|
|
381
|
+
// as the previous silent path. Treat "" the same as a non-numeric
|
|
382
|
+
// string, distinct from the legitimate undefined/null "no time given".
|
|
383
|
+
// The SDK types `book.time` as `number | undefined`, but the wire
|
|
384
|
+
// payload can return a string at runtime — we widen to unknown so the
|
|
385
|
+
// runtime guard against an empty string is meaningful (the SDK type
|
|
386
|
+
// would otherwise narrow it out).
|
|
387
|
+
const timeRaw = book.time;
|
|
388
|
+
if (timeRaw === "") {
|
|
389
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned empty string for time on ${coin}`, { exchange: "hyperliquid" });
|
|
390
|
+
}
|
|
391
|
+
const time = timeRaw === undefined || timeRaw === null ? 0 : Number(timeRaw);
|
|
392
|
+
if (!Number.isFinite(time)) {
|
|
393
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid l2Book returned non-finite time for ${coin}: ${timeRaw}`, { exchange: "hyperliquid" });
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
outcome,
|
|
397
|
+
side,
|
|
398
|
+
time,
|
|
399
|
+
bids: levels[0].map((l) => [String(l.px ?? "0"), String(l.sz ?? "0")]),
|
|
400
|
+
asks: levels[1].map((l) => [String(l.px ?? "0"), String(l.sz ?? "0")]),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async placeOrder(opts) {
|
|
404
|
+
await this.init();
|
|
405
|
+
this._validateOutcomeSide(opts.outcome, opts.side);
|
|
406
|
+
const quote = this.quoteTokenOf(opts.outcome);
|
|
407
|
+
const notional = Number(opts.price) * Number(opts.size);
|
|
408
|
+
if (!Number.isFinite(notional) || notional < MIN_ORDER_QUOTE) {
|
|
409
|
+
throw new PerpError("INVALID_PARAMS", `Outcome order value must be at least ${MIN_ORDER_QUOTE} ${quote} (got price=${opts.price} * size=${opts.size} = ${notional.toFixed(4)})`, {
|
|
410
|
+
exchange: "hyperliquid",
|
|
411
|
+
remediation: `Increase price or size so price*size >= ${MIN_ORDER_QUOTE}`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
// Pre-check the quote-token spot balance (only on buy — sell consumes outcome shares).
|
|
415
|
+
if (opts.isBuy) {
|
|
416
|
+
const available = await this._getSpotAvailable(quote);
|
|
417
|
+
if (available < notional) {
|
|
418
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient ${quote} for outcome buy: need ${notional.toFixed(4)} ${quote}, have ${available.toFixed(4)} (spot)`, {
|
|
419
|
+
exchange: "hyperliquid",
|
|
420
|
+
remediation: `Outcome ${opts.outcome} is quoted in ${quote}: hold ${quote} on Hyperliquid spot (perp-to-spot transfer or a spot swap)`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const assetId = HyperliquidOutcomeAdapter.assetId(opts.outcome, opts.side);
|
|
425
|
+
const tif = opts.tif ?? "Gtc";
|
|
426
|
+
const action = {
|
|
427
|
+
type: "order",
|
|
428
|
+
orders: [{
|
|
429
|
+
a: assetId,
|
|
430
|
+
b: opts.isBuy,
|
|
431
|
+
p: this._trimZeros(opts.price),
|
|
432
|
+
s: this._trimZeros(opts.size),
|
|
433
|
+
r: false,
|
|
434
|
+
t: { limit: { tif } },
|
|
435
|
+
}],
|
|
436
|
+
grouping: "na",
|
|
437
|
+
};
|
|
438
|
+
const result = await this._hl.exchangeAction(action);
|
|
439
|
+
HyperliquidOutcomeAdapter._assertOrderStatusOk(result);
|
|
440
|
+
await this._invalidateAccountCache();
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
async cancelOrder(outcome, side, oid) {
|
|
444
|
+
await this.init();
|
|
445
|
+
this._validateOutcomeSide(outcome, side);
|
|
446
|
+
const assetId = HyperliquidOutcomeAdapter.assetId(outcome, side);
|
|
447
|
+
const result = await this._hl.exchangeAction({
|
|
448
|
+
type: "cancel",
|
|
449
|
+
cancels: [{ a: assetId, o: oid }],
|
|
450
|
+
});
|
|
451
|
+
HyperliquidOutcomeAdapter._assertCancelStatusOk(result);
|
|
452
|
+
await this._invalidateAccountCache();
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Throw if the venue rejected the order embedded inside a top-level
|
|
457
|
+
* `status:"ok"` response (HL pattern). Returns silently for resting/filled.
|
|
458
|
+
*/
|
|
459
|
+
static _assertOrderStatusOk(result) {
|
|
460
|
+
const r = result;
|
|
461
|
+
const statuses = r?.response?.data?.statuses;
|
|
462
|
+
if (!Array.isArray(statuses) || statuses.length === 0) {
|
|
463
|
+
throw new PerpError("EXCHANGE_ERROR", `Outcome order: empty status response (${JSON.stringify(result).slice(0, 200)})`, { exchange: "hyperliquid" });
|
|
464
|
+
}
|
|
465
|
+
const st = statuses[0];
|
|
466
|
+
if (st.error) {
|
|
467
|
+
// A per-entry error inside status "ok" is the venue's definite refusal of THIS order (nothing
|
|
468
|
+
// placed) — never a retryable EXCHANGE_ERROR. Live 2026-10-12: "Insufficient spot balance asset=100014881".
|
|
469
|
+
const msg = String(st.error);
|
|
470
|
+
const insufficient = /insufficient/i.test(msg);
|
|
471
|
+
throw new PerpError(insufficient ? "INSUFFICIENT_BALANCE" : "ORDER_REJECTED", `Outcome order rejected: ${msg}`, {
|
|
472
|
+
exchange: "hyperliquid", venueError: msg,
|
|
473
|
+
...(insufficient ? { remediation: "Check the outcome's quote token balance on Hyperliquid spot: perp outcome list (quote per outcome)" } : {}),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
static _assertCancelStatusOk(result) {
|
|
478
|
+
const r = result;
|
|
479
|
+
const statuses = r?.response?.data?.statuses;
|
|
480
|
+
if (!Array.isArray(statuses) || statuses.length === 0) {
|
|
481
|
+
throw new PerpError("EXCHANGE_ERROR", `Outcome cancel: empty status response`, { exchange: "hyperliquid" });
|
|
482
|
+
}
|
|
483
|
+
const st = statuses[0];
|
|
484
|
+
if (typeof st === "object" && st !== null && "error" in st) {
|
|
485
|
+
const msg = String(st.error);
|
|
486
|
+
throw new PerpError("ORDER_REJECTED", `Outcome cancel rejected: ${msg}`, { exchange: "hyperliquid", venueError: msg });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async getOpenOrders() {
|
|
490
|
+
await this.init();
|
|
491
|
+
const orders = await this._hl.getOpenOrders();
|
|
492
|
+
return orders.filter((o) => typeof o.symbol === "string" && o.symbol.startsWith("#"));
|
|
493
|
+
}
|
|
494
|
+
// ── Private ──────────────────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* The outcome's quote token from outcomeMeta (`quoteToken`, e.g. "USDC"). Unknown outcome →
|
|
497
|
+
* SYMBOL_NOT_FOUND; missing / non-string quoteToken → EXCHANGE_ERROR (never an assumed token).
|
|
498
|
+
*/
|
|
499
|
+
quoteTokenOf(outcome) {
|
|
500
|
+
const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
|
|
501
|
+
if (!o) {
|
|
502
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, { exchange: "hyperliquid", remediation: "Run: perp outcome list" });
|
|
503
|
+
}
|
|
504
|
+
if (typeof o.quoteToken !== "string" || o.quoteToken.trim() === "") {
|
|
505
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid outcomeMeta outcome ${outcome} has no quoteToken (got ${JSON.stringify(o.quoteToken ?? null)}) — its collateral token is unknown`, { exchange: "hyperliquid" });
|
|
506
|
+
}
|
|
507
|
+
return o.quoteToken;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The outcome / side behind an outcome coin ("#14881" = outcome 1488, side 1) with its names from
|
|
511
|
+
* outcomeMeta; null when the coin is not an outcome coin or the outcome is not listed.
|
|
512
|
+
*/
|
|
513
|
+
describeCoin(coin) {
|
|
514
|
+
if (!/^[#+]\d+$/.test(coin))
|
|
515
|
+
return null;
|
|
516
|
+
const enc = Number(coin.slice(1));
|
|
517
|
+
const outcome = Math.floor(enc / 10);
|
|
518
|
+
const side = enc % 10;
|
|
519
|
+
const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
|
|
520
|
+
const sideName = o?.sideSpecs[side]?.name;
|
|
521
|
+
if (!o || sideName === undefined)
|
|
522
|
+
return null;
|
|
523
|
+
return { outcome, side, name: o.name, sideName };
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Price of held outcome balance coins (`+<encoding>`, spotClearinghouseState) in the outcome's quote
|
|
527
|
+
* token: the side's mid `allMids["#<encoding>"]` (docs info-endpoint "Retrieve mids for all coins";
|
|
528
|
+
* live 2026-10-13 mainnet allMids "#14880": "0.100405", "#14881": "0.899595"). HIP-4 docs: a side
|
|
529
|
+
* token settles to `settleFraction` (Yes) / `1 - settleFraction` (No) quote tokens and the Yes/No
|
|
530
|
+
* books are merged (buy Yes at p = sell No at 1 - p), so the mid is the token's value in its quote.
|
|
531
|
+
* `quoteToken` null = the outcome / side is not in outcomeMeta (outcomeMeta is re-read once when a
|
|
532
|
+
* coin's outcome is missing — outcomes are deployed after init) or has no quoteToken; `mid` null = no
|
|
533
|
+
* positive mid. Coins that are not `+<digits>` are not returned. A malformed allMids → EXCHANGE_ERROR.
|
|
534
|
+
*/
|
|
535
|
+
async getBalanceCoinPrices(coins) {
|
|
536
|
+
await this.init();
|
|
537
|
+
const held = coins.filter((c) => /^\+\d+$/.test(c)).map((coin) => {
|
|
538
|
+
const enc = Number(coin.slice(1));
|
|
539
|
+
return { coin, outcome: Math.floor(enc / 10), side: enc % 10, enc };
|
|
540
|
+
});
|
|
541
|
+
if (held.length === 0)
|
|
542
|
+
return [];
|
|
543
|
+
const find = (outcome) => this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
|
|
544
|
+
if (held.some((h) => find(h.outcome) === undefined))
|
|
545
|
+
await this._loadOutcomeMeta();
|
|
546
|
+
const allMids = await this._infoPost({ type: "allMids" });
|
|
547
|
+
if (allMids === null || typeof allMids !== "object" || Array.isArray(allMids)) {
|
|
548
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid allMids returned unexpected shape: ${JSON.stringify(allMids).slice(0, 200)}`, { exchange: "hyperliquid" });
|
|
549
|
+
}
|
|
550
|
+
const mids = allMids;
|
|
551
|
+
return held.map((h) => {
|
|
552
|
+
const o = find(h.outcome);
|
|
553
|
+
const quoteToken = o !== undefined && h.side < o.sideSpecs.length && typeof o.quoteToken === "string" && o.quoteToken.trim() !== ""
|
|
554
|
+
? o.quoteToken : null;
|
|
555
|
+
const raw = mids[`#${h.enc}`];
|
|
556
|
+
const mid = typeof raw === "string" && Number(raw) > 0 ? raw : null;
|
|
557
|
+
return { coin: h.coin, quoteToken, mid };
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
_lookupSideName(outcome, side) {
|
|
561
|
+
const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
|
|
562
|
+
return o?.sideSpecs[side]?.name ?? `side${side}`;
|
|
563
|
+
}
|
|
564
|
+
_validateOutcomeSide(outcome, side) {
|
|
565
|
+
HyperliquidOutcomeAdapter._assertOutcomeRange(outcome, side);
|
|
566
|
+
const o = this._outcomeMeta?.outcomes.find((x) => x.outcome === outcome);
|
|
567
|
+
if (!o) {
|
|
568
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Unknown outcome id: ${outcome}`, {
|
|
569
|
+
exchange: "hyperliquid",
|
|
570
|
+
remediation: "Run: perp outcome list",
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (side >= o.sideSpecs.length) {
|
|
574
|
+
throw new PerpError("INVALID_PARAMS", `Invalid side ${side} for outcome ${outcome} (valid: 0..${o.sideSpecs.length - 1})`, {
|
|
575
|
+
exchange: "hyperliquid",
|
|
576
|
+
remediation: "Run: perp outcome list",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Resolve the master EVM address for spot-clearinghouse queries.
|
|
582
|
+
* `HyperliquidAdapter._address` is populated only when a master/PK signer
|
|
583
|
+
* is configured. Agent-only setups (no master pk in OWS) leave it empty
|
|
584
|
+
* even though the agent meta knows the user's EVM address. Fall through
|
|
585
|
+
* to the agent registry rather than treating empty as zero balance —
|
|
586
|
+
* Rule #2: no silent fallback.
|
|
587
|
+
*/
|
|
588
|
+
async _resolveUserAddress() {
|
|
589
|
+
if (this._hl.address)
|
|
590
|
+
return this._hl.address;
|
|
591
|
+
const { getAgent } = await import("../agent-wallet/store.js");
|
|
592
|
+
const agent = getAgent("hyperliquid");
|
|
593
|
+
if (agent?.userEvmAddress)
|
|
594
|
+
return agent.userEvmAddress;
|
|
595
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "Hyperliquid user address not resolved — cannot query the spot balance state", { exchange: "hyperliquid", remediation: "Configure an OWS master wallet or register an HL agent" });
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Available spot balance of `coin` (total − hold, spotClearinghouseState). A coin the account does
|
|
599
|
+
* not hold is not listed → 0 held. No balances list, or an unreadable total / hold → EXCHANGE_ERROR.
|
|
600
|
+
*/
|
|
601
|
+
async _getSpotAvailable(coin) {
|
|
602
|
+
const userAddress = await this._resolveUserAddress();
|
|
603
|
+
const state = await this._infoPost({ type: "spotClearinghouseState", user: userAddress });
|
|
604
|
+
if (!Array.isArray(state?.balances)) {
|
|
605
|
+
throw new PerpError("EXCHANGE_ERROR", "Hyperliquid spotClearinghouseState has no balances list", { exchange: "hyperliquid" });
|
|
606
|
+
}
|
|
607
|
+
const row = state.balances.find((b) => String(b.coin) === coin);
|
|
608
|
+
if (!row)
|
|
609
|
+
return 0;
|
|
610
|
+
const total = Number(row.total);
|
|
611
|
+
const hold = Number(row.hold);
|
|
612
|
+
if (!Number.isFinite(total) || !Number.isFinite(hold)) {
|
|
613
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid spot ${coin} balance unreadable (total ${JSON.stringify(row.total)}, hold ${JSON.stringify(row.hold)})`, { exchange: "hyperliquid" });
|
|
614
|
+
}
|
|
615
|
+
return Math.max(0, total - hold);
|
|
616
|
+
}
|
|
617
|
+
async _invalidateAccountCache() {
|
|
618
|
+
try {
|
|
619
|
+
const { invalidateCache } = await import("../cache.js");
|
|
620
|
+
invalidateCache("acct");
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
// Cache module unavailable in test contexts; ignore.
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
_trimZeros(s) {
|
|
627
|
+
if (!s.includes("."))
|
|
628
|
+
return s;
|
|
629
|
+
const n = s.replace(/\.?0+$/, "");
|
|
630
|
+
return n === "" || n === "-0" ? "0" : n;
|
|
631
|
+
}
|
|
632
|
+
async _infoPost(body) {
|
|
633
|
+
const baseUrl = this._hl.isTestnet
|
|
634
|
+
? "https://api.hyperliquid-testnet.xyz"
|
|
635
|
+
: "https://api.hyperliquid.xyz";
|
|
636
|
+
const res = await fetch(`${baseUrl}/info`, {
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers: { "Content-Type": "application/json" },
|
|
639
|
+
body: JSON.stringify(body),
|
|
640
|
+
});
|
|
641
|
+
if (!res.ok) {
|
|
642
|
+
throw new PerpError("EXCHANGE_ERROR", `Hyperliquid /info HTTP ${res.status} for ${JSON.stringify(body)}`, { exchange: "hyperliquid", status: res.status });
|
|
643
|
+
}
|
|
644
|
+
return res.json();
|
|
645
|
+
}
|
|
646
|
+
}
|