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,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder fees — one uniform, default-OFF mechanism for every venue.
|
|
3
|
+
*
|
|
4
|
+
* A builder fee is an extra per-order fee the venue pays to a third-party "builder"
|
|
5
|
+
* (Hyperliquid builder codes, Pacifica builder program, Aster builder, Ondo builder
|
|
6
|
+
* codes). It is charged to the USER, so perp-cli never attaches one unless:
|
|
7
|
+
* 1. `settings.builder.<venue>` is set explicitly by the user (absent = off — there
|
|
8
|
+
* is no shipped default and no env var), and
|
|
9
|
+
* 2. the venue approval the fee needs is CONFIRMED (venue status read, cached per
|
|
10
|
+
* process for BUILDER_APPROVAL_TTL_MS; a builder-related order rejection drops the
|
|
11
|
+
* cached read). Approval-free venues (Ondo) are confirmed by (1) alone.
|
|
12
|
+
* A builder-related order rejection is returned to the caller with remediation
|
|
13
|
+
* (--no-builder / re-approve) and is NEVER retried — re-sending is a new order.
|
|
14
|
+
* An order whose approval is missing / unknown is sent WITHOUT the fee and says so in
|
|
15
|
+
* `execution.notes` (and once on stderr) — the fee is the builder's, never a reason to
|
|
16
|
+
* fail or silently change the user's order. `--no-builder` disables it per command.
|
|
17
|
+
*
|
|
18
|
+
* Per-venue facts (official docs, checked 2026-10-02):
|
|
19
|
+
* - hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes
|
|
20
|
+
* ApproveBuilderFee signed by the MAIN wallet (an agent cannot sign it); order action
|
|
21
|
+
* `builder: { b: <address>, f: <tenths of a bp> }`; max 0.1% perps / 1% spot; status
|
|
22
|
+
* info `{ type: "maxBuilderFee", user, builder }`. HIP-3 dexes are not covered by the
|
|
23
|
+
* docs → never attached there.
|
|
24
|
+
* - pacifica https://pacifica.gitbook.io/docs/programs/builder-program.md
|
|
25
|
+
* approve_builder_code { builder_code, max_fee_rate } signed by the account; per-order
|
|
26
|
+
* `builder_code` on create_market / create / stop/create / positions/tpsl. TWAP
|
|
27
|
+
* (create_twap_order) does not document builder_code → never sent there.
|
|
28
|
+
* The builder sets its own fee_rate; the user's approved max_fee_rate caps it, so the
|
|
29
|
+
* code is attached only while the approved max_fee_rate ≤ the configured feeBps (the
|
|
30
|
+
* user's consent). The venue documents no ceiling → perp-cli caps the consent at 10
|
|
31
|
+
* bps. Spot rejects it (err 108).
|
|
32
|
+
* - aster https://asterdex.github.io/aster-api-website/asterCode/integration-flow/
|
|
33
|
+
* + asterdex/api-docs demo/aster-code.py: approveBuilder { builder, maxFeeRate ≤ 0.001 }
|
|
34
|
+
* master-signed; orders carry `builder` + `feeRate` (decimal) signed by the API wallet;
|
|
35
|
+
* list GET /fapi/v3/builder, revoke DELETE /fapi/v3/builder (DelBuilder). The testnet
|
|
36
|
+
* API documents no approveBuilder → testnet off.
|
|
37
|
+
* - ondo https://docs.ondoperps.xyz/api-reference/integration_guide (+ rest-spec)
|
|
38
|
+
* no user approval; per-order `builderCode: { code, feeRateBpsFractional }` on
|
|
39
|
+
* POST /v1/perps/orders and POST /v1/perps/stop_order (stops do NOT inherit it); cap
|
|
40
|
+
* 10 bps; sandbox uses the same API.
|
|
41
|
+
* - qfex https://docs.qfex.com/qfex/builder-code
|
|
42
|
+
* fee SHARE, not an extra fee: the trader pays the normal venue fee and the venue credits
|
|
43
|
+
* part of it to the builder (feeKind "share", feeBps null). No user approval. The code
|
|
44
|
+
* (UUID) rides on the Trade WebSocket auth (`params.builder_code`) for the life of the
|
|
45
|
+
* connection — never per order; an unknown code is not an error, so attribution is not
|
|
46
|
+
* confirmable from the trader side. Prod codes mean nothing on UAT → testnet off.
|
|
47
|
+
* - lighter / lighter-rh: unsupported — see UNSUPPORTED below.
|
|
48
|
+
* - arcus: unsupported — see UNSUPPORTED below.
|
|
49
|
+
*/
|
|
50
|
+
import { PerpError, classifyError, isDefiniteOrderFailure, isTransportFailure, isUnknownOutcomeServerError } from "../errors.js";
|
|
51
|
+
import { isVenueId } from "./venue-types.js";
|
|
52
|
+
export const BUILDER_FEE_CAPABILITIES = {
|
|
53
|
+
hyperliquid: {
|
|
54
|
+
approval: "master", identity: "address", perpCapBps: 10, spotCapBps: 100, hip3: false, testnet: true,
|
|
55
|
+
feeStepBps: 0.1, feeKind: "exact", docs: "https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes",
|
|
56
|
+
},
|
|
57
|
+
pacifica: {
|
|
58
|
+
approval: "master", identity: "code", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: true,
|
|
59
|
+
feeStepBps: 0.01, feeKind: "max", docs: "https://pacifica.gitbook.io/docs/programs/builder-program.md",
|
|
60
|
+
},
|
|
61
|
+
aster: {
|
|
62
|
+
approval: "master", identity: "address", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: false,
|
|
63
|
+
feeStepBps: 0.01, feeKind: "exact", docs: "https://asterdex.github.io/aster-api-website/asterCode/integration-flow/",
|
|
64
|
+
},
|
|
65
|
+
ondo: {
|
|
66
|
+
approval: "none", identity: "code", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: true,
|
|
67
|
+
feeStepBps: 0.01, feeKind: "exact", docs: "https://docs.ondoperps.xyz/api-reference/integration_guide",
|
|
68
|
+
},
|
|
69
|
+
qfex: {
|
|
70
|
+
approval: "none", identity: "code", perpCapBps: null, spotCapBps: null, hip3: null, testnet: false,
|
|
71
|
+
feeStepBps: null, feeKind: "share", docs: "https://docs.qfex.com/qfex/builder-code",
|
|
72
|
+
},
|
|
73
|
+
lighter: null,
|
|
74
|
+
"lighter-rh": null,
|
|
75
|
+
arcus: null,
|
|
76
|
+
};
|
|
77
|
+
/** Why a venue has no builder-fee support (shown in CAPABILITY_UNSUPPORTED). */
|
|
78
|
+
export const BUILDER_FEE_UNSUPPORTED = {
|
|
79
|
+
lighter: "not yet implemented in perp-cli — the official lighter-go signer can sign ApproveIntegrator and the per-order integrator fields, but perp-cli sends them as 0 (https://apidocs.lighter.xyz/docs/partner-integration)",
|
|
80
|
+
"lighter-rh": "not yet implemented in perp-cli — Lighter RH uses the same official lighter-go signer; partner-attribution fields are sent as 0 (https://apidocs.rh.lighter.xyz/docs/partner-attribution.md)",
|
|
81
|
+
arcus: "Arcus has no perps builder fee; its integrator fee applies to the spot router only and needs a partner key from Arcus (https://docs.arcus.xyz/concepts/spot-router)",
|
|
82
|
+
};
|
|
83
|
+
export function builderFeeUnsupported(venue) {
|
|
84
|
+
return new PerpError("CAPABILITY_UNSUPPORTED", `${venue} does not support builderFee — ${BUILDER_FEE_UNSUPPORTED[venue] ?? "no builder-fee mechanism"}`, {
|
|
85
|
+
venue, feature: "builderFee", reason: BUILDER_FEE_UNSUPPORTED[venue] ?? "no builder-fee mechanism",
|
|
86
|
+
remediation: `Builder fees are available on: ${builderFeeVenues().join(", ")}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function builderFeeVenues() {
|
|
90
|
+
return Object.keys(BUILDER_FEE_CAPABILITIES).filter(v => BUILDER_FEE_CAPABILITIES[v] !== null);
|
|
91
|
+
}
|
|
92
|
+
/** Capability of a venue, or CAPABILITY_UNSUPPORTED. */
|
|
93
|
+
export function requireBuilderCapability(venue) {
|
|
94
|
+
const cap = BUILDER_FEE_CAPABILITIES[venue];
|
|
95
|
+
if (cap === null)
|
|
96
|
+
throw builderFeeUnsupported(venue);
|
|
97
|
+
return cap;
|
|
98
|
+
}
|
|
99
|
+
/** The configured user-paid fee of an "exact" / "max" venue. A "share" target has none. */
|
|
100
|
+
export function requireFeeBps(t) {
|
|
101
|
+
if (t.feeBps === null)
|
|
102
|
+
throw new PerpError("INVALID_PARAMS", `${t.venue} builder attribution carries no fee (fee share)`);
|
|
103
|
+
return t.feeBps;
|
|
104
|
+
}
|
|
105
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
106
|
+
/** Pacifica: "alphanumeric, max 16 characters" (builder-program docs). */
|
|
107
|
+
const PACIFICA_CODE_RE = /^[A-Za-z0-9]{1,16}$/;
|
|
108
|
+
/** Ondo documents no format; refuse whitespace / control characters and absurd lengths. */
|
|
109
|
+
const ONDO_CODE_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
110
|
+
/** QFEX builder codes are UUIDs (docs builder-code / authenticate). */
|
|
111
|
+
const QFEX_CODE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
112
|
+
/**
|
|
113
|
+
* Validate one settings.builder.<venue> entry. Throws `invalid(problem)` — never clamps
|
|
114
|
+
* or rewrites a value. Returns the normalized target.
|
|
115
|
+
*/
|
|
116
|
+
export function parseBuilderEntry(venue, raw, invalid) {
|
|
117
|
+
if (!isVenueId(venue))
|
|
118
|
+
throw invalid(`builder: unknown venue "${venue}"`);
|
|
119
|
+
const cap = BUILDER_FEE_CAPABILITIES[venue];
|
|
120
|
+
if (cap === null)
|
|
121
|
+
throw invalid(`builder.${venue}: ${venue} does not support builder fees — ${BUILDER_FEE_UNSUPPORTED[venue] ?? ""}`);
|
|
122
|
+
if (cap.feeKind === "share") {
|
|
123
|
+
// Fee share: the code only. A feeBps key is refused — there is no user-paid fee to configure.
|
|
124
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
125
|
+
throw invalid(`builder.${venue} must be { code: <uuid> } (fee share — no feeBps)`);
|
|
126
|
+
}
|
|
127
|
+
const s = raw;
|
|
128
|
+
for (const k of Object.keys(s)) {
|
|
129
|
+
if (k !== "code")
|
|
130
|
+
throw invalid(`builder.${venue}: unknown key "${k}" (expected code only — ${venue} builder codes share the venue fee, the user pays nothing extra)`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof s.code !== "string" || !QFEX_CODE_RE.test(s.code)) {
|
|
133
|
+
throw invalid(`builder.${venue}.code must be a UUID (got ${JSON.stringify(s.code)})`);
|
|
134
|
+
}
|
|
135
|
+
return { venue, identity: s.code.toLowerCase(), feeBps: null };
|
|
136
|
+
}
|
|
137
|
+
if (cap.perpCapBps === null || cap.feeStepBps === null) {
|
|
138
|
+
throw invalid(`builder.${venue}: capability declares no fee cap / increment`);
|
|
139
|
+
}
|
|
140
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
141
|
+
throw invalid(`builder.${venue} must be { ${cap.identity}: <${cap.identity}>, feeBps: <number> }`);
|
|
142
|
+
}
|
|
143
|
+
const e = raw;
|
|
144
|
+
for (const k of Object.keys(e)) {
|
|
145
|
+
if (k !== cap.identity && k !== "feeBps")
|
|
146
|
+
throw invalid(`builder.${venue}: unknown key "${k}" (expected ${cap.identity}, feeBps)`);
|
|
147
|
+
}
|
|
148
|
+
const id = e[cap.identity];
|
|
149
|
+
let identity;
|
|
150
|
+
if (cap.identity === "address") {
|
|
151
|
+
if (typeof id !== "string" || !ADDRESS_RE.test(id))
|
|
152
|
+
throw invalid(`builder.${venue}.address must be a 0x-prefixed 20-byte EVM address (got ${JSON.stringify(id)})`);
|
|
153
|
+
identity = id.toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const re = venue === "pacifica" ? PACIFICA_CODE_RE : ONDO_CODE_RE;
|
|
157
|
+
if (typeof id !== "string" || !re.test(id)) {
|
|
158
|
+
throw invalid(`builder.${venue}.code must match ${re.source} (got ${JSON.stringify(id)})`);
|
|
159
|
+
}
|
|
160
|
+
identity = id;
|
|
161
|
+
}
|
|
162
|
+
const fee = e.feeBps;
|
|
163
|
+
if (typeof fee !== "number" || !Number.isFinite(fee) || fee <= 0) {
|
|
164
|
+
throw invalid(`builder.${venue}.feeBps must be a number of basis points > 0 (got ${JSON.stringify(fee)})`);
|
|
165
|
+
}
|
|
166
|
+
if (fee > cap.perpCapBps) {
|
|
167
|
+
throw invalid(`builder.${venue}.feeBps ${fee} exceeds the ${venue} cap of ${cap.perpCapBps} bps`);
|
|
168
|
+
}
|
|
169
|
+
const steps = fee / cap.feeStepBps;
|
|
170
|
+
if (Math.abs(steps - Math.round(steps)) > 1e-9) {
|
|
171
|
+
throw invalid(`builder.${venue}.feeBps ${fee} is not a multiple of the venue's ${cap.feeStepBps} bps increment`);
|
|
172
|
+
}
|
|
173
|
+
return { venue, identity, feeBps: fee };
|
|
174
|
+
}
|
|
175
|
+
/** Validate the whole settings.builder object (absent / null = nothing configured). */
|
|
176
|
+
export function parseBuilderSettings(raw, invalid) {
|
|
177
|
+
const out = {};
|
|
178
|
+
if (raw === undefined || raw === null)
|
|
179
|
+
return out;
|
|
180
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
181
|
+
throw invalid("builder must be an object of { venue: { address|code, feeBps } }");
|
|
182
|
+
for (const [venue, entry] of Object.entries(raw)) {
|
|
183
|
+
const t = parseBuilderEntry(venue, entry, invalid);
|
|
184
|
+
const cap = BUILDER_FEE_CAPABILITIES[t.venue];
|
|
185
|
+
const stored = entry;
|
|
186
|
+
out[venue] = cap.feeKind === "share"
|
|
187
|
+
? { code: stored.code }
|
|
188
|
+
: cap.identity === "address"
|
|
189
|
+
? { address: stored.address, feeBps: t.feeBps }
|
|
190
|
+
: { code: stored.code, feeBps: t.feeBps };
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/** "2.5 bps", or "fee share (no extra fee)" for a share target. */
|
|
195
|
+
export function feeText(t) {
|
|
196
|
+
return t.feeBps === null ? "fee share (no extra fee)" : `${t.feeBps} bps`;
|
|
197
|
+
}
|
|
198
|
+
/** The configured target for a venue, or null (off). */
|
|
199
|
+
export function builderTargetOf(builder, venue) {
|
|
200
|
+
const entry = builder[venue];
|
|
201
|
+
if (entry === undefined)
|
|
202
|
+
return null;
|
|
203
|
+
return parseBuilderEntry(venue, entry, p => new PerpError("INVALID_PARAMS", p));
|
|
204
|
+
}
|
|
205
|
+
export const NO_BUILDER = { applied: null, note: null };
|
|
206
|
+
/** How long a venue approval read is reused before it is read again. */
|
|
207
|
+
export const BUILDER_APPROVAL_TTL_MS = 5 * 60_000;
|
|
208
|
+
/** Process-wide approval cache: `${venue}:${network}:${account}:${identity}` → status read + read time. */
|
|
209
|
+
const _approvalCache = new Map();
|
|
210
|
+
const _warned = new Set();
|
|
211
|
+
/** Forget cached approval reads and stderr warnings (after approve / revoke; test hook). */
|
|
212
|
+
export function resetBuilderApprovalCache() {
|
|
213
|
+
_approvalCache.clear();
|
|
214
|
+
_warned.clear();
|
|
215
|
+
}
|
|
216
|
+
/** Drop the cached approval reads of one venue + network (the next order re-reads the venue). */
|
|
217
|
+
export function invalidateBuilderApproval(venue, network) {
|
|
218
|
+
const prefix = `${venue}:${network}:`;
|
|
219
|
+
for (const k of [..._approvalCache.keys()])
|
|
220
|
+
if (k.startsWith(prefix))
|
|
221
|
+
_approvalCache.delete(k);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Per-adapter builder-fee gate. Built only when settings.builder.<venue> is set
|
|
225
|
+
* (resolveBuilderGate) — an unconfigured venue has no gate and no notes at all.
|
|
226
|
+
*/
|
|
227
|
+
export class BuilderFeeGate {
|
|
228
|
+
target;
|
|
229
|
+
network;
|
|
230
|
+
skipReason;
|
|
231
|
+
readApproval;
|
|
232
|
+
accountOf;
|
|
233
|
+
constructor(target, network,
|
|
234
|
+
/** Static reason the configured fee is never attached by this adapter (--no-builder, testnet). */
|
|
235
|
+
skipReason,
|
|
236
|
+
/** Reads the venue approval for the active account; null when the venue has no approval step. */
|
|
237
|
+
readApproval,
|
|
238
|
+
/** Account the approval belongs to (master address) — part of the cache key. */
|
|
239
|
+
accountOf) {
|
|
240
|
+
this.target = target;
|
|
241
|
+
this.network = network;
|
|
242
|
+
this.skipReason = skipReason;
|
|
243
|
+
this.readApproval = readApproval;
|
|
244
|
+
this.accountOf = accountOf;
|
|
245
|
+
}
|
|
246
|
+
get capability() {
|
|
247
|
+
return requireBuilderCapability(this.target.venue);
|
|
248
|
+
}
|
|
249
|
+
/** Venue approval for the configured builder (cached for BUILDER_APPROVAL_TTL_MS). Failures → "unknown". */
|
|
250
|
+
async approval() {
|
|
251
|
+
if (this.readApproval === null)
|
|
252
|
+
return { status: "approved", maxFeeBps: null };
|
|
253
|
+
const key = `${this.target.venue}:${this.network}:${this.accountOf().toLowerCase()}:${this.target.identity}`;
|
|
254
|
+
const cached = _approvalCache.get(key);
|
|
255
|
+
if (cached !== undefined && Date.now() - cached.at < BUILDER_APPROVAL_TTL_MS)
|
|
256
|
+
return cached.read;
|
|
257
|
+
const read = this.readApproval(this.target).then(a => a,
|
|
258
|
+
// Recorded, not hidden: the decision carries the reason and the order goes out without the fee.
|
|
259
|
+
(e) => ({ status: "unknown", reason: e instanceof Error ? e.message : String(e) }));
|
|
260
|
+
_approvalCache.set(key, { read, at: Date.now() });
|
|
261
|
+
return read;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether the next perp order carries the fee. `hip3` = the order is on a Hyperliquid
|
|
265
|
+
* HIP-3 dex. Never throws: a fee that cannot be attached becomes a note.
|
|
266
|
+
*/
|
|
267
|
+
async decide(opts) {
|
|
268
|
+
const t = this.target;
|
|
269
|
+
const cap = this.capability;
|
|
270
|
+
const skip = (why) => this.warnOnce({ applied: null, note: `builder fee not attached: ${why}` });
|
|
271
|
+
if (this.skipReason !== null)
|
|
272
|
+
return { applied: null, note: `builder fee not attached: ${this.skipReason}` };
|
|
273
|
+
if (opts.hip3 && cap.hip3 === false)
|
|
274
|
+
return skip("HIP-3 dex orders do not carry builder fees (not covered by the venue docs)");
|
|
275
|
+
const a = await this.approval();
|
|
276
|
+
if (a.status === "unknown") {
|
|
277
|
+
return skip(`${t.venue} approval for ${t.identity} could not be confirmed (${a.reason}) — check: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder status ${t.venue}`);
|
|
278
|
+
}
|
|
279
|
+
if (a.status === "notApproved") {
|
|
280
|
+
const have = a.maxFeeBps === null ? "none" : `${a.maxFeeBps} bps`;
|
|
281
|
+
return skip(`${t.venue} approval for ${t.identity} missing (approved max: ${have}, configured: ${feeText(t)}) — approve with: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder approve ${t.venue}`);
|
|
282
|
+
}
|
|
283
|
+
if (a.status === "aboveConsent") {
|
|
284
|
+
return skip(`${t.venue} approved max for ${t.identity} is ${a.maxFeeBps} bps, above the configured ${feeText(t)} — the builder could charge more than you configured; re-approve exactly ${feeText(t)}: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder approve ${t.venue}`);
|
|
285
|
+
}
|
|
286
|
+
return { applied: { venue: t.venue, identity: t.identity, feeBps: t.feeBps, feeKind: cap.feeKind, approvedMaxBps: a.maxFeeBps }, note: null };
|
|
287
|
+
}
|
|
288
|
+
warnOnce(d) {
|
|
289
|
+
const key = `${this.target.venue}:${this.network}:${d.note}`;
|
|
290
|
+
if (d.note !== null && !_warned.has(key)) {
|
|
291
|
+
_warned.add(key);
|
|
292
|
+
process.stderr.write(`[warn] ${d.note}\n`);
|
|
293
|
+
}
|
|
294
|
+
return d;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The gate for an adapter being created, or null when settings.builder.<venue> is not
|
|
299
|
+
* set (default) or the context is read-only. The single place builder settings are
|
|
300
|
+
* resolved (adapters never read settings).
|
|
301
|
+
*/
|
|
302
|
+
export function resolveBuilderGate(venue, ctx, builder, readApproval, accountOf) {
|
|
303
|
+
if (ctx.readOnly)
|
|
304
|
+
return null;
|
|
305
|
+
const target = builderTargetOf(builder, venue);
|
|
306
|
+
if (target === null)
|
|
307
|
+
return null;
|
|
308
|
+
const cap = requireBuilderCapability(venue);
|
|
309
|
+
let skipReason = null;
|
|
310
|
+
if (ctx.noBuilder)
|
|
311
|
+
skipReason = "disabled by --no-builder";
|
|
312
|
+
else if (ctx.network === "testnet" && !cap.testnet)
|
|
313
|
+
skipReason = `${venue} testnet has no builder-fee flow`;
|
|
314
|
+
return new BuilderFeeGate(target, ctx.network, skipReason, cap.approval === "none" ? null : readApproval, accountOf);
|
|
315
|
+
}
|
|
316
|
+
/** Gate held by an adapter (public `builderFee` field), or null. */
|
|
317
|
+
export function builderGateOf(adapter) {
|
|
318
|
+
const g = adapter?.builderFee;
|
|
319
|
+
return g instanceof BuilderFeeGate ? g : null;
|
|
320
|
+
}
|
|
321
|
+
/** Decision for an adapter's next perp order (no gate = not configured = NO_BUILDER). */
|
|
322
|
+
export async function builderDecisionOf(adapter, opts) {
|
|
323
|
+
const g = builderGateOf(adapter);
|
|
324
|
+
return g === null ? NO_BUILDER : g.decide(opts);
|
|
325
|
+
}
|
|
326
|
+
// ── Unit conversions (one place) ──
|
|
327
|
+
/** Fee rate as a decimal fraction string: 2.5 bps → "0.00025". */
|
|
328
|
+
export function bpsToRate(bps) {
|
|
329
|
+
const s = (bps / 10_000).toFixed(10).replace(/0+$/, "").replace(/\.$/, "");
|
|
330
|
+
return s === "" ? "0" : s;
|
|
331
|
+
}
|
|
332
|
+
/** Decimal fraction (string | number) → bps, or null when unreadable. */
|
|
333
|
+
export function rateToBps(rate) {
|
|
334
|
+
const n = typeof rate === "number" ? rate : typeof rate === "string" && rate.trim() !== "" ? Number(rate) : NaN;
|
|
335
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n * 10_000 * 1e6) / 1e6 : null;
|
|
336
|
+
}
|
|
337
|
+
/** Hyperliquid `f` (tenths of a basis point). */
|
|
338
|
+
export function hlFeeTenthsBps(bps) {
|
|
339
|
+
return Math.round(bps * 10);
|
|
340
|
+
}
|
|
341
|
+
/** Hyperliquid ApproveBuilderFee maxFeeRate: a percent string ("0.01%" = 1 bp). */
|
|
342
|
+
export function hlMaxFeeRatePercent(bps) {
|
|
343
|
+
return `${bpsToRate(bps * 100)}%`;
|
|
344
|
+
}
|
|
345
|
+
/** feeKind "exact": approval verdict from the venue's approved ceiling (bps; null = no approval on record). */
|
|
346
|
+
export function approvalFromMax(maxFeeBps, feeBps) {
|
|
347
|
+
// 1e-9 absorbs decimal-string → float conversion; never rounds a smaller approval up.
|
|
348
|
+
if (maxFeeBps !== null && maxFeeBps + 1e-9 >= feeBps)
|
|
349
|
+
return { status: "approved", maxFeeBps };
|
|
350
|
+
return { status: "notApproved", maxFeeBps };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* feeKind "max" (Pacifica): the builder picks its own rate up to the approved ceiling, so the
|
|
354
|
+
* configured feeBps is the user's consent and the approved ceiling must not exceed it.
|
|
355
|
+
* null = no approval on record.
|
|
356
|
+
*/
|
|
357
|
+
export function approvalFromCeiling(maxFeeBps, feeBps) {
|
|
358
|
+
if (maxFeeBps === null)
|
|
359
|
+
return { status: "notApproved", maxFeeBps: null };
|
|
360
|
+
if (maxFeeBps - 1e-9 <= feeBps)
|
|
361
|
+
return { status: "approved", maxFeeBps };
|
|
362
|
+
return { status: "aboveConsent", maxFeeBps };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* An order the venue rejected for a builder-related reason (the error text names the builder):
|
|
366
|
+
* the cached approval of that venue is dropped (the next order re-reads it) and the rejection is
|
|
367
|
+
* returned with remediation. The order is NOT retried — re-sending is a new order, so that stays
|
|
368
|
+
* the user's explicit choice. Any other error (or no gate) is returned unchanged.
|
|
369
|
+
*/
|
|
370
|
+
export function builderRejectionOf(adapter, err) {
|
|
371
|
+
const g = builderGateOf(adapter);
|
|
372
|
+
if (g === null)
|
|
373
|
+
return err;
|
|
374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
375
|
+
if (!/builder/i.test(msg))
|
|
376
|
+
return err;
|
|
377
|
+
// Only a DEFINITE venue rejection is re-described: an unknown outcome (ORDER_PENDING /
|
|
378
|
+
// ORDER_PARTIAL, a transport failure, a 5xx, any non-definite PerpError) keeps its own
|
|
379
|
+
// "do NOT resubmit" remediation — "re-send it without the fee" could double the order.
|
|
380
|
+
if (!isDefiniteBuilderRejection(err))
|
|
381
|
+
return err;
|
|
382
|
+
const venue = g.target.venue;
|
|
383
|
+
invalidateBuilderApproval(venue, g.network);
|
|
384
|
+
const n = g.network === "testnet" ? " -n testnet" : "";
|
|
385
|
+
const remediation = `The venue rejected the order over the builder fee; it was NOT retried. Re-send it without the fee (--no-builder), or re-approve: perp${n} wallet builder approve ${venue} (check: perp${n} wallet builder status ${venue})`;
|
|
386
|
+
const e = classifyError(err, venue);
|
|
387
|
+
return new PerpError(e.code, e.message, { ...(e.details ?? {}), exchange: venue, remediation });
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* true when `err` proves the order was refused (nothing placed): a PerpError with a definite code
|
|
391
|
+
* (isDefiniteOrderFailure), or a venue 4xx answer (details.httpStatus 4xx, not 429) whose code is
|
|
392
|
+
* not an unknown-outcome code; a plain Error that is neither a transport failure nor a 5xx
|
|
393
|
+
* unknown-outcome message (adapter-level venue refusal text).
|
|
394
|
+
*/
|
|
395
|
+
function isDefiniteBuilderRejection(err) {
|
|
396
|
+
if (err instanceof PerpError) {
|
|
397
|
+
const code = err.structured.code;
|
|
398
|
+
if (code === "ORDER_PENDING" || code === "ORDER_PARTIAL")
|
|
399
|
+
return false;
|
|
400
|
+
if (isDefiniteOrderFailure(err))
|
|
401
|
+
return true;
|
|
402
|
+
const http = err.structured.details?.httpStatus;
|
|
403
|
+
return typeof http === "number" && http >= 400 && http < 500 && http !== 429;
|
|
404
|
+
}
|
|
405
|
+
return !isTransportFailure(err) && !isUnknownOutcomeServerError(err);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Builder decision for a planned perp order on `symbol` (dry-run / preview): the same
|
|
409
|
+
* cached gate decision a live order would use. Hyperliquid resolves the asset id so a
|
|
410
|
+
* HIP-3 market reports "not attached".
|
|
411
|
+
*/
|
|
412
|
+
export async function builderPlanFor(adapter, symbol) {
|
|
413
|
+
if (builderGateOf(adapter) === null)
|
|
414
|
+
return NO_BUILDER;
|
|
415
|
+
const hl = adapter;
|
|
416
|
+
if (hl.name === "hyperliquid" && typeof hl.builderDecision === "function" && typeof hl.getAssetIndex === "function") {
|
|
417
|
+
const idx = symbol === null ? null : await hl.getAssetIndex.call(adapter, symbol.toUpperCase());
|
|
418
|
+
return hl.builderDecision.call(adapter, idx);
|
|
419
|
+
}
|
|
420
|
+
return builderDecisionOf(adapter, { hip3: false });
|
|
421
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-venue canonical symbols — the single mapping source used to decide
|
|
3
|
+
* that two markets on different venues are the same underlying
|
|
4
|
+
* (design §4 L4, §10.9).
|
|
5
|
+
*
|
|
6
|
+
* Rules by asset class:
|
|
7
|
+
* - crypto: ticker identity after the adapter strips its quote suffix, plus
|
|
8
|
+
* DENOMINATION_ALIASES (kPEPE ≡ 1000PEPE, UBTC ≡ BTC ...).
|
|
9
|
+
* - equity / etf: exchange-listed ticker identity (TSLA on Ondo ≡ TSLA on
|
|
10
|
+
* Arcus ≡ TSLA on Lighter RH ≡ xyz:TSLA on HIP-3). Adapters must
|
|
11
|
+
* pass the plain ticker (e.g. strip Ondo's "-USD.P").
|
|
12
|
+
* - index / commodity / fx / preIpo: EXPLICIT table only. Same-looking names
|
|
13
|
+
* are frequently different products (US500 vs USA500, USOIL vs CL),
|
|
14
|
+
* so an unmapped market returns null and is excluded from matching.
|
|
15
|
+
*/
|
|
16
|
+
import type { AssetClass, VenueId } from "./venue-types.js";
|
|
17
|
+
/** Denomination / wrapper aliases → canonical crypto ticker. */
|
|
18
|
+
export declare const DENOMINATION_ALIASES: Readonly<Record<string, string>>;
|
|
19
|
+
/**
|
|
20
|
+
* Explicit mapping for index / commodity / fx / preIpo markets.
|
|
21
|
+
* Key: `${venue}|${symbol}` (symbol as the adapter reports it; for HIP-3 use `hl:<dex>` as venue).
|
|
22
|
+
*/
|
|
23
|
+
export declare const EXPLICIT_CANONICAL: Readonly<Record<string, string>>;
|
|
24
|
+
/** Pairs verified to be DIFFERENT products despite similar names. Never match. */
|
|
25
|
+
export declare const NEVER_MATCH: ReadonlySet<string>;
|
|
26
|
+
export declare function isNeverMatch(a: string, b: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Canonical key for a market, or null when it must not be matched across venues.
|
|
29
|
+
* @param venueKey venue id, or `hl:<dex>` for HIP-3 dex markets
|
|
30
|
+
* @param symbol plain symbol as reported by the adapter (no quote suffix, no dex prefix)
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalFor(venueKey: VenueId | `hl:${string}`, symbol: string, assetClass: AssetClass): string | null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-venue canonical symbols — the single mapping source used to decide
|
|
3
|
+
* that two markets on different venues are the same underlying
|
|
4
|
+
* (design §4 L4, §10.9).
|
|
5
|
+
*
|
|
6
|
+
* Rules by asset class:
|
|
7
|
+
* - crypto: ticker identity after the adapter strips its quote suffix, plus
|
|
8
|
+
* DENOMINATION_ALIASES (kPEPE ≡ 1000PEPE, UBTC ≡ BTC ...).
|
|
9
|
+
* - equity / etf: exchange-listed ticker identity (TSLA on Ondo ≡ TSLA on
|
|
10
|
+
* Arcus ≡ TSLA on Lighter RH ≡ xyz:TSLA on HIP-3). Adapters must
|
|
11
|
+
* pass the plain ticker (e.g. strip Ondo's "-USD.P").
|
|
12
|
+
* - index / commodity / fx / preIpo: EXPLICIT table only. Same-looking names
|
|
13
|
+
* are frequently different products (US500 vs USA500, USOIL vs CL),
|
|
14
|
+
* so an unmapped market returns null and is excluded from matching.
|
|
15
|
+
*/
|
|
16
|
+
/** Denomination / wrapper aliases → canonical crypto ticker. */
|
|
17
|
+
export const DENOMINATION_ALIASES = {
|
|
18
|
+
KPEPE: "1000PEPE",
|
|
19
|
+
"1000PEPE": "1000PEPE",
|
|
20
|
+
KSHIB: "1000SHIB",
|
|
21
|
+
KBONK: "1000BONK",
|
|
22
|
+
KFLOKI: "1000FLOKI",
|
|
23
|
+
KLUNC: "1000LUNC",
|
|
24
|
+
KNEIRO: "1000NEIRO",
|
|
25
|
+
KDOGS: "1000DOGS",
|
|
26
|
+
// Hyperliquid spot wrapper tokens (was SPOT_PERP_TOKEN_MAP)
|
|
27
|
+
UBTC: "BTC",
|
|
28
|
+
UETH: "ETH",
|
|
29
|
+
USOL: "SOL",
|
|
30
|
+
UFART: "FARTCOIN",
|
|
31
|
+
UPUMP: "PUMP",
|
|
32
|
+
LINK0: "LINK",
|
|
33
|
+
AVAX0: "AVAX",
|
|
34
|
+
AAVE0: "AAVE",
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Explicit mapping for index / commodity / fx / preIpo markets.
|
|
38
|
+
* Key: `${venue}|${symbol}` (symbol as the adapter reports it; for HIP-3 use `hl:<dex>` as venue).
|
|
39
|
+
*/
|
|
40
|
+
export const EXPLICIT_CANONICAL = {
|
|
41
|
+
// Gold / silver spot-referenced perps
|
|
42
|
+
"ondo|XAU": "GOLD",
|
|
43
|
+
"ondo|XAG": "SILVER",
|
|
44
|
+
"lighter-rh|XAU": "GOLD",
|
|
45
|
+
"lighter-rh|XAG": "SILVER",
|
|
46
|
+
"hl:xyz|GOLD": "GOLD",
|
|
47
|
+
"hl:xyz|SILVER": "SILVER",
|
|
48
|
+
"qfex|GOLD": "GOLD",
|
|
49
|
+
"qfex|SILVER": "SILVER",
|
|
50
|
+
// Crude oil (WTI)
|
|
51
|
+
"ondo|WTI": "CRUDE_OIL_WTI",
|
|
52
|
+
"hl:xyz|CL": "CRUDE_OIL_WTI",
|
|
53
|
+
"hl:flx|OIL": "CRUDE_OIL_WTI",
|
|
54
|
+
// QFEX CL blends expiring futures; roll-week gaps are caught by the cross-venue price filter.
|
|
55
|
+
"qfex|CL": "CRUDE_OIL_WTI",
|
|
56
|
+
// US equity indices
|
|
57
|
+
"ondo|US500": "SPX",
|
|
58
|
+
"ondo|US100": "NDX",
|
|
59
|
+
"qfex|US500": "SPX",
|
|
60
|
+
"qfex|US100": "NDX",
|
|
61
|
+
// FX
|
|
62
|
+
"ondo|USDJPY": "USDJPY",
|
|
63
|
+
};
|
|
64
|
+
/** Pairs verified to be DIFFERENT products despite similar names. Never match. */
|
|
65
|
+
export const NEVER_MATCH = new Set([
|
|
66
|
+
"USAR:US500", "USAR:USA500", "US500:USA500",
|
|
67
|
+
"SEMI:SEMIS",
|
|
68
|
+
"USOIL:CL", "USOIL:OIL",
|
|
69
|
+
"GOLDJM:GOLD", "GLDMINE:GOLD",
|
|
70
|
+
"SILVERJM:SILVER",
|
|
71
|
+
"URNM:URANIUM",
|
|
72
|
+
"NUCLEAR:ENERGY", "NUCLEAR:USENERGY",
|
|
73
|
+
]);
|
|
74
|
+
export function isNeverMatch(a, b) {
|
|
75
|
+
return NEVER_MATCH.has(`${a}:${b}`) || NEVER_MATCH.has(`${b}:${a}`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Canonical key for a market, or null when it must not be matched across venues.
|
|
79
|
+
* @param venueKey venue id, or `hl:<dex>` for HIP-3 dex markets
|
|
80
|
+
* @param symbol plain symbol as reported by the adapter (no quote suffix, no dex prefix)
|
|
81
|
+
*/
|
|
82
|
+
export function canonicalFor(venueKey, symbol, assetClass) {
|
|
83
|
+
const up = symbol.toUpperCase();
|
|
84
|
+
switch (assetClass) {
|
|
85
|
+
case "crypto":
|
|
86
|
+
return DENOMINATION_ALIASES[up] ?? up; // ticker identity is the rule for crypto; the alias table overrides it
|
|
87
|
+
case "equity":
|
|
88
|
+
case "etf":
|
|
89
|
+
return up;
|
|
90
|
+
case "index":
|
|
91
|
+
case "commodity":
|
|
92
|
+
case "fx":
|
|
93
|
+
case "preIpo":
|
|
94
|
+
return EXPLICIT_CANONICAL[`${venueKey}|${up}`] ?? null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -56,7 +56,8 @@ export interface EvmAddressCapable {
|
|
|
56
56
|
readonly address: string;
|
|
57
57
|
}
|
|
58
58
|
export interface QueryOrderCapable {
|
|
59
|
-
|
|
59
|
+
/** Order id (u64) or, on Hyperliquid, a client order id (cloid) string. */
|
|
60
|
+
queryOrder(orderId: number | string): Promise<unknown>;
|
|
60
61
|
}
|
|
61
62
|
export interface UsdTransferCapable {
|
|
62
63
|
usdTransfer(amount: number, destination: string): Promise<unknown>;
|
|
@@ -72,3 +73,8 @@ export declare function hasLighterAccount(adapter: ExchangeAdapter): adapter is
|
|
|
72
73
|
export declare function hasEvmAddress(adapter: ExchangeAdapter): adapter is ExchangeAdapter & EvmAddressCapable;
|
|
73
74
|
export declare function isQueryOrderCapable(adapter: ExchangeAdapter): adapter is ExchangeAdapter & QueryOrderCapable;
|
|
74
75
|
export declare function isUsdTransferCapable(adapter: ExchangeAdapter): adapter is ExchangeAdapter & UsdTransferCapable;
|
|
76
|
+
/**
|
|
77
|
+
* true when the venue's kline endpoint needs account credentials (Ondo GET /v1/perps/candles):
|
|
78
|
+
* `market kline` then uses the credentialed adapter instead of the keyless public one.
|
|
79
|
+
*/
|
|
80
|
+
export declare function klinesNeedAuth(adapter: ExchangeAdapter): boolean;
|
|
@@ -42,3 +42,10 @@ export function isQueryOrderCapable(adapter) {
|
|
|
42
42
|
export function isUsdTransferCapable(adapter) {
|
|
43
43
|
return typeof adapter.usdTransfer === "function";
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* true when the venue's kline endpoint needs account credentials (Ondo GET /v1/perps/candles):
|
|
47
|
+
* `market kline` then uses the credentialed adapter instead of the keyless public one.
|
|
48
|
+
*/
|
|
49
|
+
export function klinesNeedAuth(adapter) {
|
|
50
|
+
return adapter.klinesRequireAuth === true;
|
|
51
|
+
}
|