perp-cli 0.12.17 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +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 +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +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 +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +142 -55
- package/skills/perp-cli/references/agent-operations.md +110 -56
- package/skills/perp-cli/references/commands.md +266 -71
- 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,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aster Spot Adapter (design §12, §12.4 #8).
|
|
3
|
+
*
|
|
4
|
+
* Host `sapi.asterdex.com` (testnet `sapi.asterdex-testnet.com`), paths
|
|
5
|
+
* `/api/v3/*`. Signing is the perp EIP-712 Domain B scheme, reused through
|
|
6
|
+
* `AsterAdapter.signedRequest()` (same agent signer + nonce counter).
|
|
7
|
+
*
|
|
8
|
+
* Account model is separate: spot balances live in `/api/v3/account`; funds
|
|
9
|
+
* move with `POST /api/v3/asset/wallet/transfer` (FUTURE_SPOT / SPOT_FUTURE).
|
|
10
|
+
*
|
|
11
|
+
* Venue rules enforced locally (official spot V3 docs):
|
|
12
|
+
* - TIF GTC | IOC | GTX (post-only). FOK is rejected by the venue (-1115).
|
|
13
|
+
* - LIMIT price must be on PRICE_FILTER.tickSize (never rounded).
|
|
14
|
+
* - Market orders are IOC LIMIT at a protection price (design §13.2): bookTicker
|
|
15
|
+
* mid ± slippage, aligned toward crossing on tickSize, inside PRICE_FILTER
|
|
16
|
+
* min/max and PERCENT_PRICE (venue native MARKET is never sent).
|
|
17
|
+
* - quantity on LOT_SIZE step, min, max (MARKET_LOT_SIZE only for native MARKET).
|
|
18
|
+
* - MIN_NOTIONAL / NOTIONAL minimum when a price is known.
|
|
19
|
+
*
|
|
20
|
+
* Permission gate: when the active signer is an agent whose stored meta lacks
|
|
21
|
+
* `permissions.canSpotTrade === true`, trading calls throw POLICY_DENIED. Venue
|
|
22
|
+
* permission rejections (-1002 / -2015) are mapped to the same error.
|
|
23
|
+
*/
|
|
24
|
+
import { unsupported } from "./trading-v2.js";
|
|
25
|
+
import { asterIocOutcome } from "./aster.js";
|
|
26
|
+
import { PerpError, isTransportFailure, orderNotSent, orderOutcomeUnknown, sendFailure, unknownActionRemediation } from "../errors.js";
|
|
27
|
+
import { alignProtectionPrice, exactUnits, floorSize, requireOnTick, requirePositive, slippagePrice, subDecimal } from "./precision.js";
|
|
28
|
+
/** Default quote for a bare base symbol (= venue spot quoteAssets[0]). */
|
|
29
|
+
const DEFAULT_QUOTE = "USDT";
|
|
30
|
+
const SPOT_REMEDIATION = "perp wallet agent rotate aster --can-spot";
|
|
31
|
+
/** Venue codes that mean "this signer may not do this" (spot V3 error table). */
|
|
32
|
+
const PERMISSION_CODES = new Set(["-1002", "-2015"]);
|
|
33
|
+
function decimalsOf(step) {
|
|
34
|
+
const frac = step.split(".")[1];
|
|
35
|
+
return frac ? frac.replace(/0+$/, "").length : 0;
|
|
36
|
+
}
|
|
37
|
+
/** Exact decimal comparison: -1 | 0 | 1. */
|
|
38
|
+
function cmp(a, b) {
|
|
39
|
+
const d = subDecimal(a, b);
|
|
40
|
+
if (d.startsWith("-"))
|
|
41
|
+
return -1;
|
|
42
|
+
return d === "0" ? 0 : 1;
|
|
43
|
+
}
|
|
44
|
+
function addDecimal(a, b) {
|
|
45
|
+
return subDecimal(a, b.startsWith("-") ? b.slice(1) : `-${b}`);
|
|
46
|
+
}
|
|
47
|
+
function isZero(v) {
|
|
48
|
+
return cmp(v, "0") === 0;
|
|
49
|
+
}
|
|
50
|
+
function venueCodeOf(e) {
|
|
51
|
+
if (!(e instanceof PerpError))
|
|
52
|
+
return null;
|
|
53
|
+
const c = e.structured.details?.venueCode;
|
|
54
|
+
return c === undefined || c === null ? null : String(c);
|
|
55
|
+
}
|
|
56
|
+
function str(v, field) {
|
|
57
|
+
if (typeof v === "string")
|
|
58
|
+
return v;
|
|
59
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
60
|
+
return String(v);
|
|
61
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot: missing or invalid field "${field}"`, { exchange: "aster" });
|
|
62
|
+
}
|
|
63
|
+
function lotFilter(f, name) {
|
|
64
|
+
return { step: str(f.stepSize, `${name}.stepSize`), min: str(f.minQty, `${name}.minQty`), max: str(f.maxQty, `${name}.maxQty`) };
|
|
65
|
+
}
|
|
66
|
+
export class AsterSpotAdapter {
|
|
67
|
+
name = "aster";
|
|
68
|
+
_perp;
|
|
69
|
+
/** "BASE/QUOTE" → market */
|
|
70
|
+
_markets = new Map();
|
|
71
|
+
/** wire "BTCUSDT" → market */
|
|
72
|
+
_byWire = new Map();
|
|
73
|
+
_initialized = false;
|
|
74
|
+
_tranCounter = 0;
|
|
75
|
+
constructor(perp) {
|
|
76
|
+
this._perp = perp;
|
|
77
|
+
}
|
|
78
|
+
async init() {
|
|
79
|
+
if (this._initialized)
|
|
80
|
+
return;
|
|
81
|
+
await this._loadExchangeInfo();
|
|
82
|
+
this._initialized = true;
|
|
83
|
+
}
|
|
84
|
+
// ── Market data ──
|
|
85
|
+
async getSpotMarkets() {
|
|
86
|
+
const [tickers, prices] = await Promise.all([
|
|
87
|
+
this._publicGet("/api/v3/ticker/24hr", {}),
|
|
88
|
+
this._publicGet("/api/v3/ticker/price", {}),
|
|
89
|
+
]);
|
|
90
|
+
if (!Array.isArray(tickers) || !Array.isArray(prices)) {
|
|
91
|
+
throw new PerpError("EXCHANGE_ERROR", "Aster /api/v3/ticker/24hr or /ticker/price did not return an array", { exchange: "aster" });
|
|
92
|
+
}
|
|
93
|
+
const dayByWire = new Map(tickers.map((t) => [String(t.symbol), t]));
|
|
94
|
+
const priceByWire = new Map(prices.map((t) => [String(t.symbol), t]));
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const m of this._markets.values()) {
|
|
97
|
+
if (m.status !== "TRADING")
|
|
98
|
+
continue;
|
|
99
|
+
// Price source is /ticker/price: it covers every listed symbol (never-traded
|
|
100
|
+
// ones report type "listing_price"), unlike /ticker/24hr.
|
|
101
|
+
const p = priceByWire.get(m.wire);
|
|
102
|
+
if (!p) {
|
|
103
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot: no /ticker/price row for TRADING symbol ${m.wire}`, { exchange: "aster" });
|
|
104
|
+
}
|
|
105
|
+
// /ticker/24hr omits symbols with no trade in the window (live: the
|
|
106
|
+
// per-symbol query returns {}); an absent row is reported as null (not published).
|
|
107
|
+
const day = dayByWire.get(m.wire);
|
|
108
|
+
const volume24h = day === undefined ? null : str(day.quoteVolume, `${m.wire}.quoteVolume`);
|
|
109
|
+
out.push({
|
|
110
|
+
symbol: `${m.base}/${m.quote}`,
|
|
111
|
+
baseToken: m.base,
|
|
112
|
+
quoteToken: m.quote,
|
|
113
|
+
markPrice: str(p.price, `${m.wire}.price`),
|
|
114
|
+
volume24h,
|
|
115
|
+
sizeDecimals: decimalsOf(m.lot.step),
|
|
116
|
+
priceDecimals: decimalsOf(m.tickSize),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
async getSpotOrderbook(symbol) {
|
|
122
|
+
const m = this._resolve(symbol);
|
|
123
|
+
const res = await this._publicGet("/api/v3/depth", { symbol: m.wire, limit: "50" });
|
|
124
|
+
if (!Array.isArray(res.bids) || !Array.isArray(res.asks)) {
|
|
125
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster /api/v3/depth returned no bids/asks for ${m.wire}`, { exchange: "aster" });
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
bids: res.bids.map(([p, q]) => [String(p), String(q)]),
|
|
129
|
+
asks: res.asks.map(([p, q]) => [String(p), String(q)]),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// ── Account ──
|
|
133
|
+
async getSpotBalances() {
|
|
134
|
+
const acct = await this._signed("GET", "/api/v3/account", {});
|
|
135
|
+
if (!Array.isArray(acct.balances)) {
|
|
136
|
+
throw new PerpError("EXCHANGE_ERROR", "Aster /api/v3/account returned no balances array", { exchange: "aster" });
|
|
137
|
+
}
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const b of acct.balances) {
|
|
140
|
+
const asset = str(b.asset, "balance.asset");
|
|
141
|
+
const free = str(b.free, `${asset}.free`);
|
|
142
|
+
const locked = str(b.locked, `${asset}.locked`);
|
|
143
|
+
const total = addDecimal(free, locked);
|
|
144
|
+
if (isZero(total))
|
|
145
|
+
continue;
|
|
146
|
+
// Separate spot wallet — never the same money as perp equity.
|
|
147
|
+
out.push({ token: asset, total, available: free, held: locked, shared: false });
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
// ── Trading ──
|
|
152
|
+
async spotMarketOrder(symbol, side, size, opts) {
|
|
153
|
+
// §13.2: IOC LIMIT at the protection price, never native MARKET (no price cap).
|
|
154
|
+
// Pre-send (market, size, price, min notional, permission): a failure is a definite "not placed".
|
|
155
|
+
let m;
|
|
156
|
+
let qty;
|
|
157
|
+
let px;
|
|
158
|
+
let protection;
|
|
159
|
+
try {
|
|
160
|
+
m = this._resolveTrading(symbol);
|
|
161
|
+
// A LIMIT order: LOT_SIZE applies (MARKET_LOT_SIZE is for type MARKET only).
|
|
162
|
+
qty = this._sizeFor(m, size, false);
|
|
163
|
+
protection = await this.computeProtectionPrice(symbol, side, opts.slippage);
|
|
164
|
+
px = protection.protectionPrice;
|
|
165
|
+
const minNotional = this._minNotional(m, false);
|
|
166
|
+
if (minNotional !== null)
|
|
167
|
+
this._checkNotional(m, px, qty, minNotional);
|
|
168
|
+
this._requireSpotPermission();
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
throw orderNotSent("aster", `spot ${side} ${size} ${symbol}`, e);
|
|
172
|
+
}
|
|
173
|
+
// market=true: a NEW / PARTIALLY_FILLED IOC response is not final yet → "pending".
|
|
174
|
+
const r = await this._submitOrder(m, {
|
|
175
|
+
symbol: m.wire,
|
|
176
|
+
side: side.toUpperCase(),
|
|
177
|
+
type: "LIMIT",
|
|
178
|
+
timeInForce: "IOC",
|
|
179
|
+
quantity: qty,
|
|
180
|
+
price: px,
|
|
181
|
+
}, true);
|
|
182
|
+
return { ...r, venueRaw: { response: r.venueRaw, protection } };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Declared protection-price reference for spot: the bookTicker mid
|
|
186
|
+
* ((bidPrice + askPrice) / 2, GET /api/v3/ticker/bookTicker). Spot has no mark price;
|
|
187
|
+
* no runtime substitute — a missing / one-sided book → PRICE_STALE.
|
|
188
|
+
*/
|
|
189
|
+
async referencePrice(symbol) {
|
|
190
|
+
const m = this._resolve(symbol);
|
|
191
|
+
let bid;
|
|
192
|
+
let ask;
|
|
193
|
+
try {
|
|
194
|
+
const t = await this._publicGet("/api/v3/ticker/bookTicker", { symbol: m.wire });
|
|
195
|
+
bid = Number(t.bidPrice);
|
|
196
|
+
ask = Number(t.askPrice);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
throw new PerpError("PRICE_STALE", `Aster spot ${m.wire} bookTicker unavailable: ${e instanceof Error ? e.message : String(e)}`, { exchange: "aster" });
|
|
200
|
+
}
|
|
201
|
+
if (!(bid > 0) || !(ask > 0) || !Number.isFinite(bid) || !Number.isFinite(ask)) {
|
|
202
|
+
throw new PerpError("PRICE_STALE", `Aster spot ${m.wire} has no two-sided book (bid ${bid}, ask ${ask})`, { exchange: "aster" });
|
|
203
|
+
}
|
|
204
|
+
return String((bid + ask) / 2);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Protection price a spot market order would be sent with now: mid ± slippage.bps,
|
|
208
|
+
* aligned toward crossing on PRICE_FILTER.tickSize; outside PRICE_FILTER min/max or
|
|
209
|
+
* PERCENT_PRICE (checked against the mid) → INVALID_PARAMS, never clamped.
|
|
210
|
+
*/
|
|
211
|
+
async computeProtectionPrice(symbol, side, slippage) {
|
|
212
|
+
const m = this._resolve(symbol);
|
|
213
|
+
const ref = await this.referencePrice(symbol);
|
|
214
|
+
const protectionPrice = alignProtectionPrice(slippagePrice(ref, side, slippage.bps), m.tickSize, side);
|
|
215
|
+
const p = Number(protectionPrice);
|
|
216
|
+
if (Number(m.minPrice) > 0 && p < Number(m.minPrice)) {
|
|
217
|
+
throw new PerpError("INVALID_PARAMS", `Aster spot ${m.wire}: price ${protectionPrice} below PRICE_FILTER minPrice ${m.minPrice}`, { exchange: "aster" });
|
|
218
|
+
}
|
|
219
|
+
if (Number(m.maxPrice) > 0 && p > Number(m.maxPrice)) {
|
|
220
|
+
throw new PerpError("INVALID_PARAMS", `Aster spot ${m.wire}: price ${protectionPrice} above PRICE_FILTER maxPrice ${m.maxPrice}`, { exchange: "aster" });
|
|
221
|
+
}
|
|
222
|
+
if (m.percent !== null) {
|
|
223
|
+
const r = Number(ref);
|
|
224
|
+
if (p > r * m.percent.up || p < r * m.percent.down) {
|
|
225
|
+
throw new PerpError("INVALID_PARAMS", `Aster spot ${m.wire}: protection price ${protectionPrice} is outside PERCENT_PRICE (${m.percent.down}..${m.percent.up} × ${ref})`, {
|
|
226
|
+
exchange: "aster",
|
|
227
|
+
remediation: "Use a smaller --slippage-bps",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const appliedBps = Math.abs(p - Number(ref)) / Number(ref) * 10_000;
|
|
232
|
+
return { referencePrice: ref, protectionPrice, appliedBps };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Definite IOC outcome (./ioc-outcome.ts contract) of what spotMarketOrder /
|
|
236
|
+
* spotLimitOrder returned — a SpotOrderResult (its venueRaw is read) or a raw venue row.
|
|
237
|
+
*/
|
|
238
|
+
iocOutcomeOf(raw) {
|
|
239
|
+
if (raw !== null && typeof raw === "object" && raw.venue === "aster" && "venueRaw" in raw) {
|
|
240
|
+
// spotMarketOrder wraps the venue response as { response, protection }.
|
|
241
|
+
const vr = raw.venueRaw;
|
|
242
|
+
const inner = vr !== null && typeof vr === "object" && "protection" in vr && "response" in vr ? vr.response : vr;
|
|
243
|
+
return asterIocOutcome(inner);
|
|
244
|
+
}
|
|
245
|
+
return asterIocOutcome(raw);
|
|
246
|
+
}
|
|
247
|
+
async spotLimitOrder(symbol, side, price, size, opts) {
|
|
248
|
+
const m = this._resolveTrading(symbol);
|
|
249
|
+
// Aster is a legacy family: limit orders keep the GTC default (design §10.7 #6).
|
|
250
|
+
const tif = this._mapTif(opts?.tif === undefined ? "GTC" : opts.tif);
|
|
251
|
+
const px = requireOnTick(price, m.tickSize);
|
|
252
|
+
const qty = this._sizeFor(m, size, false);
|
|
253
|
+
const minNotional = this._minNotional(m, false);
|
|
254
|
+
if (minNotional !== null)
|
|
255
|
+
this._checkNotional(m, px, qty, minNotional);
|
|
256
|
+
this._requireSpotPermission();
|
|
257
|
+
return this._submitOrder(m, {
|
|
258
|
+
symbol: m.wire,
|
|
259
|
+
side: side.toUpperCase(),
|
|
260
|
+
type: "LIMIT",
|
|
261
|
+
timeInForce: tif,
|
|
262
|
+
quantity: qty,
|
|
263
|
+
price: px,
|
|
264
|
+
}, false);
|
|
265
|
+
}
|
|
266
|
+
async spotCancelOrder(symbol, orderId) {
|
|
267
|
+
const m = this._resolve(symbol);
|
|
268
|
+
if (!/^\d+$/.test(orderId)) {
|
|
269
|
+
throw new PerpError("INVALID_PARAMS", `Aster orderId must be numeric (got "${orderId}")`, { exchange: "aster" });
|
|
270
|
+
}
|
|
271
|
+
this._requireSpotPermission();
|
|
272
|
+
let raw;
|
|
273
|
+
try {
|
|
274
|
+
raw = await this._signed("DELETE", "/api/v3/order", { symbol: m.wire, orderId });
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
// Spot V3 docs ("Error Codes", 20xx): "-2011 CANCEL_REJECTED — Order cancellation rejected."
|
|
278
|
+
// Same as the perp cancel: the order is not open, nothing was cancelled (definite).
|
|
279
|
+
if (venueCodeOf(e) === "-2011") {
|
|
280
|
+
throw new PerpError("ORDER_REJECTED", `Aster refused to cancel spot order ${orderId} on ${m.base}/${m.quote} (-2011 CANCEL_REJECTED): it is not an open order`, {
|
|
281
|
+
exchange: "aster", orderId, venueCode: -2011,
|
|
282
|
+
remediation: "List the open spot orders: perp -e aster trade spot-orders",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
return this._toResult(m, raw, false);
|
|
288
|
+
}
|
|
289
|
+
async getSpotOpenOrders(symbol) {
|
|
290
|
+
const params = symbol === null ? {} : { symbol: this._resolve(symbol).wire };
|
|
291
|
+
const rows = await this._signed("GET", "/api/v3/openOrders", params);
|
|
292
|
+
if (!Array.isArray(rows)) {
|
|
293
|
+
throw new PerpError("EXCHANGE_ERROR", "Aster /api/v3/openOrders did not return an array", { exchange: "aster" });
|
|
294
|
+
}
|
|
295
|
+
return rows.map((o) => {
|
|
296
|
+
const m = this._byWire.get(String(o.symbol));
|
|
297
|
+
if (!m) {
|
|
298
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot open order on unknown symbol ${String(o.symbol)}`, { exchange: "aster" });
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
orderId: String(o.orderId),
|
|
302
|
+
symbol: `${m.base}/${m.quote}`,
|
|
303
|
+
side: o.side === "BUY" ? "buy" : "sell",
|
|
304
|
+
price: str(o.price, "order.price"),
|
|
305
|
+
size: str(o.origQty, "order.origQty"),
|
|
306
|
+
filled: str(o.executedQty, "order.executedQty"),
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/** Venue requires a symbol; null cancels per symbol that has open orders. */
|
|
311
|
+
async spotCancelAllOrders(symbol) {
|
|
312
|
+
this._requireSpotPermission();
|
|
313
|
+
if (symbol !== null) {
|
|
314
|
+
return this._signed("DELETE", "/api/v3/allOpenOrders", { symbol: this._resolve(symbol).wire });
|
|
315
|
+
}
|
|
316
|
+
const open = await this.getSpotOpenOrders(null);
|
|
317
|
+
const symbols = [...new Set(open.map((o) => o.symbol))];
|
|
318
|
+
const results = [];
|
|
319
|
+
for (const s of symbols) {
|
|
320
|
+
results.push(await this._signed("DELETE", "/api/v3/allOpenOrders", { symbol: this._resolve(s).wire }));
|
|
321
|
+
}
|
|
322
|
+
return results;
|
|
323
|
+
}
|
|
324
|
+
// ── Transfer (separate account) ──
|
|
325
|
+
async transferToSpot(amount, asset) {
|
|
326
|
+
return this._transfer(amount, asset, "FUTURE_SPOT");
|
|
327
|
+
}
|
|
328
|
+
async transferToPerp(amount, asset) {
|
|
329
|
+
return this._transfer(amount, asset, "SPOT_FUTURE");
|
|
330
|
+
}
|
|
331
|
+
async _transfer(amount, asset, kindType) {
|
|
332
|
+
requirePositive(amount, "amount");
|
|
333
|
+
// Docs: the signer must be an agent approved with canSpotTrade or canPerpTrade.
|
|
334
|
+
const meta = this._perp.activeAgentMeta();
|
|
335
|
+
if (meta !== null && meta.permissions?.canSpotTrade !== true && meta.permissions?.canPerpTrade !== true) {
|
|
336
|
+
throw new PerpError("POLICY_DENIED", "Aster agent has neither spot nor perp trading permission (required for wallet transfer)", {
|
|
337
|
+
exchange: "aster",
|
|
338
|
+
remediation: SPOT_REMEDIATION,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const clientTranId = `perpcli${Date.now()}${++this._tranCounter}`;
|
|
342
|
+
const what = `wallet transfer ${kindType} ${amount} ${asset.toUpperCase()}`;
|
|
343
|
+
const remediation = `${unknownActionRemediation("aster", "transfer")} (perp -e aster spot balance / portfolio); clientTranId ${clientTranId}`;
|
|
344
|
+
let res;
|
|
345
|
+
try {
|
|
346
|
+
res = await this._signed("POST", "/api/v3/asset/wallet/transfer", {
|
|
347
|
+
amount,
|
|
348
|
+
asset: asset.toUpperCase(),
|
|
349
|
+
clientTranId,
|
|
350
|
+
kindType,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
// Same contract as _submitOrder: once signed and sent, a 5xx, an unreadable 2xx body or a
|
|
355
|
+
// transport failure (socket reset, timeout) leaves the transfer's outcome unknown →
|
|
356
|
+
// ORDER_PENDING (never resubmit). No connection at all → not sent; 4xx / venue error
|
|
357
|
+
// envelopes are definite refusals.
|
|
358
|
+
const d = e instanceof PerpError ? e.structured.details : undefined;
|
|
359
|
+
const http = typeof d?.httpStatus === "number" ? d.httpStatus : null;
|
|
360
|
+
if (d?.responseUnreadable === true || (http !== null && http >= 500))
|
|
361
|
+
throw orderOutcomeUnknown("aster", what, e, remediation);
|
|
362
|
+
if (isTransportFailure(e))
|
|
363
|
+
throw sendFailure("aster", what, e, remediation);
|
|
364
|
+
if (e instanceof PerpError && e.structured.code === "EXCHANGE_ERROR" && http !== null && http >= 400 && http < 500) {
|
|
365
|
+
throw new PerpError("ACTION_REJECTED", `Aster ${what} refused (nothing was transferred): ${e.message}`, { exchange: "aster", httpStatus: http, venueCode: d?.venueCode });
|
|
366
|
+
}
|
|
367
|
+
throw e;
|
|
368
|
+
}
|
|
369
|
+
// A completed transfer answers {"tranId": …, "status": "SUCCESS"}. No other status value is
|
|
370
|
+
// known to be a definite failure, so anything else after an accepted request is unknown.
|
|
371
|
+
if (res.status !== "SUCCESS") {
|
|
372
|
+
throw orderOutcomeUnknown("aster", what, new Error(`venue status ${JSON.stringify(res.status ?? null)} (not "SUCCESS"): ${JSON.stringify(res).slice(0, 200)}`), remediation);
|
|
373
|
+
}
|
|
374
|
+
return res;
|
|
375
|
+
}
|
|
376
|
+
// ── Internals ──
|
|
377
|
+
async _loadExchangeInfo() {
|
|
378
|
+
const info = await this._publicGet("/api/v3/exchangeInfo", {});
|
|
379
|
+
if (!Array.isArray(info.symbols)) {
|
|
380
|
+
throw new PerpError("EXCHANGE_ERROR", "Aster /api/v3/exchangeInfo returned no symbols", { exchange: "aster" });
|
|
381
|
+
}
|
|
382
|
+
this._markets.clear();
|
|
383
|
+
this._byWire.clear();
|
|
384
|
+
for (const s of info.symbols) {
|
|
385
|
+
const wire = str(s.symbol, "symbol");
|
|
386
|
+
// Docs: symbols starting with TEST are Aster-internal and must not be traded.
|
|
387
|
+
if (wire.startsWith("TEST"))
|
|
388
|
+
continue;
|
|
389
|
+
const filters = (Array.isArray(s.filters) ? s.filters : []);
|
|
390
|
+
const f = (type) => filters.find((x) => x.filterType === type) ?? null;
|
|
391
|
+
const priceF = f("PRICE_FILTER");
|
|
392
|
+
const lotF = f("LOT_SIZE");
|
|
393
|
+
if (!priceF || !lotF) {
|
|
394
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot ${wire}: missing PRICE_FILTER or LOT_SIZE`, { exchange: "aster" });
|
|
395
|
+
}
|
|
396
|
+
const ppF = f("PERCENT_PRICE");
|
|
397
|
+
let percent = null;
|
|
398
|
+
if (ppF) {
|
|
399
|
+
const up = Number(str(ppF.multiplierUp, `${wire}.PERCENT_PRICE.multiplierUp`));
|
|
400
|
+
const down = Number(str(ppF.multiplierDown, `${wire}.PERCENT_PRICE.multiplierDown`));
|
|
401
|
+
if (!(up > 0) || !(down > 0)) {
|
|
402
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot ${wire}: unreadable PERCENT_PRICE multipliers`, { exchange: "aster" });
|
|
403
|
+
}
|
|
404
|
+
percent = { up, down };
|
|
405
|
+
}
|
|
406
|
+
const mlF = f("MARKET_LOT_SIZE");
|
|
407
|
+
const mnF = f("MIN_NOTIONAL");
|
|
408
|
+
const nF = f("NOTIONAL");
|
|
409
|
+
const m = {
|
|
410
|
+
wire,
|
|
411
|
+
base: str(s.baseAsset, `${wire}.baseAsset`).toUpperCase(),
|
|
412
|
+
quote: str(s.quoteAsset, `${wire}.quoteAsset`).toUpperCase(),
|
|
413
|
+
status: str(s.status, `${wire}.status`),
|
|
414
|
+
tickSize: str(priceF.tickSize, `${wire}.tickSize`),
|
|
415
|
+
minPrice: str(priceF.minPrice, `${wire}.minPrice`),
|
|
416
|
+
maxPrice: str(priceF.maxPrice, `${wire}.maxPrice`),
|
|
417
|
+
percent,
|
|
418
|
+
lot: lotFilter(lotF, `${wire}.LOT_SIZE`),
|
|
419
|
+
marketLot: mlF ? lotFilter(mlF, `${wire}.MARKET_LOT_SIZE`) : null,
|
|
420
|
+
minNotional: mnF ? str(mnF.minNotional, `${wire}.MIN_NOTIONAL`) : null,
|
|
421
|
+
notional: nF ? { min: str(nF.minNotional, `${wire}.NOTIONAL.minNotional`), applyMinToMarket: nF.applyMinToMarket === true } : null,
|
|
422
|
+
};
|
|
423
|
+
this._markets.set(`${m.base}/${m.quote}`, m);
|
|
424
|
+
this._byWire.set(wire, m);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** bare base → BASE/USDT; "BASE/QUOTE" as given. */
|
|
428
|
+
_resolve(symbol) {
|
|
429
|
+
const parts = symbol.toUpperCase().split("/");
|
|
430
|
+
if (parts.length > 2 || !parts[0]) {
|
|
431
|
+
throw new PerpError("INVALID_PARAMS", `Aster spot symbol must be BASE or BASE/QUOTE (got "${symbol}")`, { exchange: "aster" });
|
|
432
|
+
}
|
|
433
|
+
const key = parts.length === 2 ? `${parts[0]}/${parts[1]}` : `${parts[0]}/${DEFAULT_QUOTE}`;
|
|
434
|
+
const m = this._markets.get(key);
|
|
435
|
+
if (!m) {
|
|
436
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Aster spot market ${key} not found`, {
|
|
437
|
+
exchange: "aster",
|
|
438
|
+
remediation: "List markets with the spot markets command for aster",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return m;
|
|
442
|
+
}
|
|
443
|
+
_resolveTrading(symbol) {
|
|
444
|
+
const m = this._resolve(symbol);
|
|
445
|
+
if (m.status !== "TRADING") {
|
|
446
|
+
throw unsupported("Aster", "spotMarket", `spot ${m.base}/${m.quote} is not trading (status ${m.status})`, "Pick a spot market whose status is TRADING");
|
|
447
|
+
}
|
|
448
|
+
return m;
|
|
449
|
+
}
|
|
450
|
+
_mapTif(raw) {
|
|
451
|
+
const t = raw.toUpperCase();
|
|
452
|
+
if (t === "GTC" || t === "IOC" || t === "GTX")
|
|
453
|
+
return t;
|
|
454
|
+
if (t === "ALO")
|
|
455
|
+
return "GTX";
|
|
456
|
+
if (t === "FOK") {
|
|
457
|
+
throw unsupported("Aster", "fok", "spot rejects FOK (venue returns -1115 INVALID_TIF)", "Use --tif IOC, GTC or ALO (post-only)");
|
|
458
|
+
}
|
|
459
|
+
throw new PerpError("INVALID_PARAMS", `Unsupported time-in-force "${raw}" for Aster spot`, {
|
|
460
|
+
exchange: "aster",
|
|
461
|
+
remediation: "Use --tif GTC, IOC or ALO (post-only)",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/** Floor to LOT_SIZE (+ MARKET_LOT_SIZE) step and check min/max of each applicable filter. */
|
|
465
|
+
_sizeFor(m, size, market) {
|
|
466
|
+
requirePositive(size, "size");
|
|
467
|
+
const lots = [m.lot];
|
|
468
|
+
// A zero MARKET_LOT_SIZE step means no step constraint; min/max still apply.
|
|
469
|
+
if (market && m.marketLot)
|
|
470
|
+
lots.push(m.marketLot);
|
|
471
|
+
// Floor only on a non-zero step (a zero step means no step constraint).
|
|
472
|
+
let qty = size;
|
|
473
|
+
for (const l of lots) {
|
|
474
|
+
if (!isZero(l.step))
|
|
475
|
+
qty = floorSize(qty, l.step);
|
|
476
|
+
}
|
|
477
|
+
for (const l of lots) {
|
|
478
|
+
const name = l === m.lot ? "LOT_SIZE" : "MARKET_LOT_SIZE";
|
|
479
|
+
if (cmp(qty, l.min) < 0) {
|
|
480
|
+
throw new PerpError("SIZE_TOO_SMALL", `Aster spot ${m.wire}: quantity ${qty} below ${name} minQty ${l.min}`, {
|
|
481
|
+
exchange: "aster",
|
|
482
|
+
remediation: `Use a size of at least ${l.min}`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (!isZero(l.max) && cmp(qty, l.max) > 0) {
|
|
486
|
+
throw new PerpError("SIZE_TOO_LARGE", `Aster spot ${m.wire}: quantity ${qty} above ${name} maxQty ${l.max}`, {
|
|
487
|
+
exchange: "aster",
|
|
488
|
+
remediation: `Split the order into sizes of at most ${l.max}`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
// Venue rule: (quantity - minQty) % stepSize == 0
|
|
492
|
+
if (!isZero(l.step) && exactUnits(subDecimal(qty, l.min), l.step) === null) {
|
|
493
|
+
throw new PerpError("INVALID_PARAMS", `Aster spot ${m.wire}: quantity ${qty} is off the ${name} grid (minQty ${l.min}, step ${l.step})`, {
|
|
494
|
+
exchange: "aster",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return qty;
|
|
499
|
+
}
|
|
500
|
+
/** Effective minimum notional, or null when no filter applies. */
|
|
501
|
+
_minNotional(m, market) {
|
|
502
|
+
const mins = [];
|
|
503
|
+
if (m.minNotional !== null)
|
|
504
|
+
mins.push(m.minNotional);
|
|
505
|
+
if (m.notional !== null && (!market || m.notional.applyMinToMarket))
|
|
506
|
+
mins.push(m.notional.min);
|
|
507
|
+
if (mins.length === 0)
|
|
508
|
+
return null;
|
|
509
|
+
return mins.reduce((a, b) => (cmp(a, b) >= 0 ? a : b));
|
|
510
|
+
}
|
|
511
|
+
_checkNotional(m, price, qty, minNotional) {
|
|
512
|
+
const notional = Number(price) * Number(qty);
|
|
513
|
+
if (!Number.isFinite(notional) || notional < Number(minNotional)) {
|
|
514
|
+
throw new PerpError("SIZE_TOO_SMALL", `Aster spot ${m.wire}: notional ${notional} below minimum ${minNotional} ${m.quote}`, {
|
|
515
|
+
exchange: "aster",
|
|
516
|
+
remediation: `Increase size so price × size ≥ ${minNotional} ${m.quote}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** §12.4 #8: gate only when the active signer is an agent lacking canSpotTrade. */
|
|
521
|
+
_requireSpotPermission() {
|
|
522
|
+
const meta = this._perp.activeAgentMeta();
|
|
523
|
+
if (meta !== null && meta.permissions?.canSpotTrade !== true) {
|
|
524
|
+
throw new PerpError("POLICY_DENIED", "Aster agent is not approved for spot trading (canSpotTrade=false)", {
|
|
525
|
+
exchange: "aster",
|
|
526
|
+
remediation: SPOT_REMEDIATION,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async _signed(method, path, params) {
|
|
531
|
+
try {
|
|
532
|
+
return await this._perp.signedRequest(this._perp.spotBaseUrl, method, path, params);
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
const code = venueCodeOf(e);
|
|
536
|
+
if (code !== null && PERMISSION_CODES.has(code)) {
|
|
537
|
+
throw new PerpError("POLICY_DENIED", `Aster spot rejected the signer's permissions [${code}]: ${e.message}`, {
|
|
538
|
+
exchange: "aster",
|
|
539
|
+
venueCode: code,
|
|
540
|
+
remediation: SPOT_REMEDIATION,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
throw e;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async _publicGet(path, params) {
|
|
547
|
+
const qs = new URLSearchParams(params).toString();
|
|
548
|
+
const url = `${this._perp.spotBaseUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
549
|
+
const res = await fetch(url);
|
|
550
|
+
const text = await res.text();
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
throw new PerpError(res.status === 429 ? "RATE_LIMITED" : "EXCHANGE_ERROR", `GET ${path} failed (${res.status}): ${text.slice(0, 200)}`, {
|
|
553
|
+
exchange: "aster",
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
let parsed;
|
|
557
|
+
try {
|
|
558
|
+
parsed = JSON.parse(text);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
throw new PerpError("EXCHANGE_ERROR", `GET ${path}: malformed JSON: ${text.slice(0, 200)}`, { exchange: "aster" });
|
|
562
|
+
}
|
|
563
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
564
|
+
const j = parsed;
|
|
565
|
+
if (typeof j.code === "number" && j.code < 0) {
|
|
566
|
+
throw new PerpError(j.code === -1121 ? "SYMBOL_NOT_FOUND" : "EXCHANGE_ERROR", `GET ${path}: [${j.code}] ${String(j.msg)}`, {
|
|
567
|
+
exchange: "aster",
|
|
568
|
+
venueCode: j.code,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return parsed;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* POST /api/v3/order. Outcome-unknown failures — HTTP 5xx (the gateway may have
|
|
576
|
+
* forwarded the order), a network / timeout error, or a 2xx with an unreadable
|
|
577
|
+
* body — become a "pending" result,
|
|
578
|
+
* never a thrown (retryable) EXCHANGE_ERROR: a resubmit could place a second order.
|
|
579
|
+
* 4xx / venue error envelopes are definite rejections and still throw.
|
|
580
|
+
*/
|
|
581
|
+
async _submitOrder(m, params, market) {
|
|
582
|
+
let raw;
|
|
583
|
+
try {
|
|
584
|
+
raw = await this._signed("POST", "/api/v3/order", params);
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
const d = e instanceof PerpError ? e.structured.details : undefined;
|
|
588
|
+
const http = typeof d?.httpStatus === "number" ? d.httpStatus : null;
|
|
589
|
+
// A transport failure (fetch / socket / timeout) after signing may have delivered the order too.
|
|
590
|
+
const unknownOutcome = d?.responseUnreadable === true || (http !== null && http >= 500) || isTransportFailure(e);
|
|
591
|
+
if (!unknownOutcome) {
|
|
592
|
+
// Explicit 4xx / venue error envelope: definitely not placed.
|
|
593
|
+
if (e instanceof PerpError && e.structured.code === "EXCHANGE_ERROR" && http !== null && http >= 400 && http < 500) {
|
|
594
|
+
throw new PerpError("ORDER_REJECTED", e.message, { exchange: "aster", httpStatus: http, venueCode: d?.venueCode });
|
|
595
|
+
}
|
|
596
|
+
throw e;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
venue: "aster",
|
|
600
|
+
symbol: `${m.base}/${m.quote}`,
|
|
601
|
+
orderId: null,
|
|
602
|
+
status: "pending",
|
|
603
|
+
filledSize: null,
|
|
604
|
+
avgPrice: null,
|
|
605
|
+
finality: "pending",
|
|
606
|
+
receivedToken: null,
|
|
607
|
+
remediation: `Aster order submit outcome unknown (${e instanceof Error ? e.message : String(e)}) — do NOT resubmit; check spot open orders and balances: perp -e aster trade spot-orders / spot-balance`,
|
|
608
|
+
venueRaw: null,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return this._acceptedResult(m, raw, market);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Result of an order the venue already accepted (HTTP success). A response that
|
|
615
|
+
* cannot be interpreted must not become a thrown error — a retry would place a
|
|
616
|
+
* second order — so it is reported as finality "pending" with the order id.
|
|
617
|
+
*/
|
|
618
|
+
_acceptedResult(m, raw, market) {
|
|
619
|
+
try {
|
|
620
|
+
return this._toResult(m, raw, market);
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
const r = (raw !== null && typeof raw === "object" ? raw : {});
|
|
624
|
+
const orderId = typeof r.orderId === "string" || typeof r.orderId === "number" ? String(r.orderId) : null;
|
|
625
|
+
return {
|
|
626
|
+
venue: "aster",
|
|
627
|
+
symbol: `${m.base}/${m.quote}`,
|
|
628
|
+
orderId,
|
|
629
|
+
status: "pending",
|
|
630
|
+
filledSize: null,
|
|
631
|
+
avgPrice: null,
|
|
632
|
+
finality: "pending",
|
|
633
|
+
receivedToken: null,
|
|
634
|
+
remediation: `Order was accepted by Aster but its response could not be read (${e instanceof Error ? e.message : String(e)}) — do NOT resubmit; check spot open orders and balances for aster${orderId !== null ? ` (orderId ${orderId})` : ""}`,
|
|
635
|
+
venueRaw: raw,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
_toResult(m, raw, market) {
|
|
640
|
+
const r = raw;
|
|
641
|
+
const venueStatus = str(r.status, "order.status");
|
|
642
|
+
const executed = r.executedQty !== undefined ? str(r.executedQty, "order.executedQty") : null;
|
|
643
|
+
const hasFill = executed !== null && !isZero(executed);
|
|
644
|
+
let status;
|
|
645
|
+
switch (venueStatus) {
|
|
646
|
+
case "NEW":
|
|
647
|
+
status = "open";
|
|
648
|
+
break;
|
|
649
|
+
case "PARTIALLY_FILLED":
|
|
650
|
+
status = "partial";
|
|
651
|
+
break;
|
|
652
|
+
case "FILLED":
|
|
653
|
+
status = "filled";
|
|
654
|
+
break;
|
|
655
|
+
case "CANCELED":
|
|
656
|
+
status = "cancelled";
|
|
657
|
+
break;
|
|
658
|
+
case "REJECTED":
|
|
659
|
+
status = "rejected";
|
|
660
|
+
break;
|
|
661
|
+
// EXPIRED = engine-cancelled remainder (e.g. market not fully filled).
|
|
662
|
+
case "EXPIRED":
|
|
663
|
+
status = hasFill ? "partial" : "cancelled";
|
|
664
|
+
break;
|
|
665
|
+
default:
|
|
666
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster spot: unknown order status "${venueStatus}"`, { exchange: "aster" });
|
|
667
|
+
}
|
|
668
|
+
// A MARKET order still NEW/PARTIALLY_FILLED has not reached a terminal state.
|
|
669
|
+
const pending = market && (venueStatus === "NEW" || venueStatus === "PARTIALLY_FILLED");
|
|
670
|
+
const avg = r.avgPrice !== undefined ? str(r.avgPrice, "order.avgPrice") : null;
|
|
671
|
+
return {
|
|
672
|
+
venue: "aster",
|
|
673
|
+
symbol: `${m.base}/${m.quote}`,
|
|
674
|
+
orderId: r.orderId !== undefined ? String(r.orderId) : null,
|
|
675
|
+
status,
|
|
676
|
+
filledSize: executed,
|
|
677
|
+
avgPrice: hasFill && avg !== null && !isZero(avg) ? avg : null,
|
|
678
|
+
finality: pending ? "pending" : "final",
|
|
679
|
+
receivedToken: null,
|
|
680
|
+
remediation: pending
|
|
681
|
+
? "Market order submitted but not final yet — do NOT resubmit; check spot open orders and balances for aster"
|
|
682
|
+
: null,
|
|
683
|
+
venueRaw: raw,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|