perp-cli 0.14.2 → 0.15.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 +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +168 -36
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ui.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +9 -4
- package/dist/exchanges/aster.js +125 -55
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
package/dist/exchanges/aster.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Docs: https://docs.asterdex.com/product/aster-perpetuals/api/api-documentation
|
|
19
19
|
* Base: https://fapi.asterdex.com
|
|
20
20
|
*/
|
|
21
|
+
import { nonEmptyOrListed } from "./unknown-symbol.js";
|
|
21
22
|
import { accountCacheEpoch } from "../cache.js";
|
|
22
23
|
import { isExpired } from "../agent-wallet/expiry.js";
|
|
23
24
|
import { classifyError, DEFINITE_ORDER_FAILURE_CODES, isTransportFailure, orderNotSent, orderOutcomeUnknown, PerpError, sendFailure } from "../errors.js";
|
|
@@ -30,7 +31,7 @@ import { approvalFromMax, bpsToRate, builderGateOf, NO_BUILDER, rateToBps, requi
|
|
|
30
31
|
import { ASTER_CLIENT_ID_RE, ASTER_FUNDS_CHAINS, asterVenue } from "./venues/aster.js";
|
|
31
32
|
import { assertAsterWithdrawAmount, assertAsterWithdrawSignature, parseAsterWithdrawLimits, requireAsterFundsNetwork, } from "./aster-funds.js";
|
|
32
33
|
import { nextAsterNonce } from "./aster-nonce.js";
|
|
33
|
-
/** Common TIF
|
|
34
|
+
/** Common TIF → Aster perp timeInForce. Live exchangeInfo lists GTC/IOC/GTX/HIDDEN — no FOK. */
|
|
34
35
|
function asterTif(raw) {
|
|
35
36
|
const t = raw.toUpperCase();
|
|
36
37
|
if (t === "GTC" || t === "IOC" || t === "GTX")
|
|
@@ -72,6 +73,16 @@ export function asterIocOutcome(raw) {
|
|
|
72
73
|
const filledSize = plainQty(r.executedQty);
|
|
73
74
|
return filledSize === null ? null : { filledSize, final: true };
|
|
74
75
|
}
|
|
76
|
+
/** The numeric `code` of an Aster error body ({"code":-1121,"msg":"Invalid symbol."}); null when absent. */
|
|
77
|
+
function asterErrorCode(body) {
|
|
78
|
+
try {
|
|
79
|
+
const j = JSON.parse(body);
|
|
80
|
+
return typeof j.code === "number" ? j.code : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null; // not JSON (e.g. an HTML error page): no venue code
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
/**
|
|
76
87
|
* Order-placement refusals by documented venue code (asterdex/api-docs V3 futures "Error
|
|
77
88
|
* Codes"; "Codes are universal,but messages can vary"). [PerpError code, docs name, remediation].
|
|
@@ -159,6 +170,10 @@ export function requireAsterClientOrderId(id) {
|
|
|
159
170
|
* that the read refuses (EXCHANGE_ERROR) rather than returning a partial history.
|
|
160
171
|
*/
|
|
161
172
|
export const ASTER_INCOME_MAX_PAGES = 60;
|
|
173
|
+
/** GET /fapi/v3/klines maximum rows per request (docs: limit max 1500). */
|
|
174
|
+
const ASTER_KLINE_PAGE = 1500;
|
|
175
|
+
/** Page cap for one range (150 000 bars). */
|
|
176
|
+
const ASTER_KLINE_MAX_PAGES = 100;
|
|
162
177
|
export class AsterAdapter {
|
|
163
178
|
name = "aster";
|
|
164
179
|
chain = "bnb";
|
|
@@ -279,7 +294,7 @@ export class AsterAdapter {
|
|
|
279
294
|
return null;
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
|
-
// ── Spot host access (
|
|
297
|
+
// ── Spot host access (used by AsterSpotAdapter) ──
|
|
283
298
|
/**
|
|
284
299
|
* Aster spot REST host. Spot shares the perp EIP-712 Domain B signing
|
|
285
300
|
* (chainId 1666 mainnet / 714 testnet) and the per-agent nonce space.
|
|
@@ -375,41 +390,54 @@ export class AsterAdapter {
|
|
|
375
390
|
// math depends on.
|
|
376
391
|
this._loadFundingInfo(),
|
|
377
392
|
]);
|
|
393
|
+
if (!Array.isArray(info?.symbols)) {
|
|
394
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster exchangeInfo has no symbol list (${JSON.stringify(info).slice(0, 120)})`, { exchange: "aster" });
|
|
395
|
+
}
|
|
396
|
+
if (!Array.isArray(tickers)) {
|
|
397
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster 24h ticker is not a list (${JSON.stringify(tickers).slice(0, 120)})`, { exchange: "aster" });
|
|
398
|
+
}
|
|
378
399
|
const tickerMap = new Map();
|
|
379
|
-
for (const t of tickers
|
|
400
|
+
for (const t of tickers) {
|
|
380
401
|
tickerMap.set(String(t.symbol), t);
|
|
381
402
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
403
|
+
// The mark price comes from premiumIndex only: a failed read throws (never a "0" / last-trade
|
|
404
|
+
// stand-in), and a symbol missing from it is left out of the list below.
|
|
405
|
+
const premiums = await this._publicGet("/fapi/v3/premiumIndex");
|
|
406
|
+
if (!Array.isArray(premiums)) {
|
|
407
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster premiumIndex is not a list (${JSON.stringify(premiums).slice(0, 120)})`, { exchange: "aster" });
|
|
408
|
+
}
|
|
409
|
+
const premiumMap = new Map(premiums.map(p => [String(p.symbol), p]));
|
|
410
|
+
this._ingestPriceFilters(info.symbols);
|
|
411
|
+
// USDT-quoted perps only: the CLI symbol mapping (ETH ↔ ETHUSDT) cannot address the USD1 / U
|
|
412
|
+
// quoted markets (live 2026-10-13: 22 USD1 + 2 U), so listing them would offer untradeable rows.
|
|
413
|
+
const tradingSymbols = info.symbols
|
|
414
|
+
.filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING" && String(s.quoteAsset) === "USDT");
|
|
415
|
+
const result = tradingSymbols.flatMap((s) => {
|
|
392
416
|
const sym = String(s.symbol);
|
|
393
417
|
const ticker = tickerMap.get(sym);
|
|
394
418
|
const premium = premiumMap.get(sym);
|
|
395
|
-
const
|
|
419
|
+
const mark = Number(premium?.markPrice);
|
|
420
|
+
if (premium === undefined || !(mark > 0))
|
|
421
|
+
return []; // no published mark price → not listed at 0
|
|
422
|
+
// Public exchangeInfo carries no leverage (the bracket endpoint is signed) → unknown.
|
|
423
|
+
const maxLev = null;
|
|
396
424
|
const lotFilter = s.filters
|
|
397
425
|
?.find(f => f.filterType === "LOT_SIZE");
|
|
398
426
|
const fundingHours = this._fundingHoursCache.get(this._fromApi(sym));
|
|
399
|
-
return {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
427
|
+
return [{
|
|
428
|
+
symbol: this._fromApi(sym),
|
|
429
|
+
markPrice: String(premium.markPrice),
|
|
430
|
+
indexPrice: premium?.indexPrice != null ? String(premium.indexPrice) : null,
|
|
431
|
+
fundingRate: premium?.lastFundingRate != null ? String(premium.lastFundingRate) : null,
|
|
432
|
+
volume24h: ticker?.quoteVolume != null ? String(ticker.quoteVolume) : null,
|
|
433
|
+
// /fapi/v3/exchangeInfo and ticker/24hr carry no open interest.
|
|
434
|
+
openInterest: null,
|
|
435
|
+
maxLeverage: maxLev,
|
|
436
|
+
sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : null,
|
|
437
|
+
stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : null,
|
|
438
|
+
fundingHours: fundingHours ?? null,
|
|
439
|
+
marginTiers: null,
|
|
440
|
+
}];
|
|
413
441
|
});
|
|
414
442
|
this._marketsCache = result;
|
|
415
443
|
this._marketsCacheTime = Date.now();
|
|
@@ -449,7 +477,7 @@ export class AsterAdapter {
|
|
|
449
477
|
this._ingestPriceFilters(info.symbols);
|
|
450
478
|
}
|
|
451
479
|
if (!this._priceFilters.has(apiSymbol)) {
|
|
452
|
-
throw new PerpError("SYMBOL_NOT_FOUND", `Aster market ${apiSymbol} not found in exchangeInfo`, { exchange: "aster" });
|
|
480
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Aster market ${apiSymbol} not found in exchangeInfo`, { exchange: "aster", remediation: "List markets: perp -e aster market list" });
|
|
453
481
|
}
|
|
454
482
|
const f = this._priceFilters.get(apiSymbol);
|
|
455
483
|
if (!f) {
|
|
@@ -458,7 +486,7 @@ export class AsterAdapter {
|
|
|
458
486
|
return f;
|
|
459
487
|
}
|
|
460
488
|
/**
|
|
461
|
-
* Declared protection-price reference
|
|
489
|
+
* Declared protection-price reference: the venue mark price from
|
|
462
490
|
* GET /fapi/v3/premiumIndex — the same price PERCENT_PRICE is evaluated against.
|
|
463
491
|
* No runtime substitute (no mid / last fallback): unavailable → PRICE_STALE.
|
|
464
492
|
*/
|
|
@@ -538,7 +566,7 @@ export class AsterAdapter {
|
|
|
538
566
|
side: t.isBuyerMaker ? "sell" : "buy",
|
|
539
567
|
price: String(t.price ?? "0"),
|
|
540
568
|
size: String(t.qty ?? "0"),
|
|
541
|
-
fee:
|
|
569
|
+
fee: null, // the public tape carries no fee
|
|
542
570
|
}));
|
|
543
571
|
}
|
|
544
572
|
async getFundingHistory(symbol, limit = 10) {
|
|
@@ -546,29 +574,66 @@ export class AsterAdapter {
|
|
|
546
574
|
symbol: this._toApi(symbol),
|
|
547
575
|
limit: String(limit),
|
|
548
576
|
});
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
577
|
+
if (!Array.isArray(data)) {
|
|
578
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster fundingRate history is not a list (${JSON.stringify(data).slice(0, 120)})`, { exchange: "aster" });
|
|
579
|
+
}
|
|
580
|
+
// `200 []` for an unlisted symbol (live 2026-10-14): checked against the market directory.
|
|
581
|
+
return nonEmptyOrListed(this, symbol, data.map((d) => {
|
|
582
|
+
const time = Number(d.fundingTime);
|
|
583
|
+
if (d.fundingTime === undefined || d.fundingTime === null || !Number.isFinite(time) || time <= 0) {
|
|
584
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster fundingRate row without a numeric fundingTime: ${JSON.stringify(d).slice(0, 160)}`, { exchange: "aster" });
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
time,
|
|
588
|
+
rate: venueRateField(d.fundingRate, "fundingRate fundingRate", "aster"),
|
|
589
|
+
price: d.markPrice ? String(d.markPrice) : null,
|
|
590
|
+
};
|
|
591
|
+
}), this._fromApi(this._toApi(symbol)));
|
|
554
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Candles over [startTime, endTime]. GET /fapi/v3/klines returns at most 1500 bars per request
|
|
595
|
+
* (1501 → -1130), so the range is read in pages that start after the last bar returned — a long
|
|
596
|
+
* range is never cut to the first page. More than ASTER_KLINE_MAX_PAGES pages → INVALID_PARAMS.
|
|
597
|
+
*/
|
|
555
598
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
599
|
+
if (!(startTime <= endTime)) {
|
|
600
|
+
throw new PerpError("INVALID_PARAMS", `Aster klines: start ${startTime} is after end ${endTime}`, {
|
|
601
|
+
remediation: "Pass a --start that is before --end",
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const out = [];
|
|
605
|
+
let from = startTime;
|
|
606
|
+
for (let page = 0; from <= endTime; page++) {
|
|
607
|
+
if (page >= ASTER_KLINE_MAX_PAGES) {
|
|
608
|
+
throw new PerpError("INVALID_PARAMS", `Aster: ${interval} candles over this range need more than ${ASTER_KLINE_MAX_PAGES} requests`, {
|
|
609
|
+
remediation: "Narrow --start / --end or use a longer interval",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const data = await this._publicGet("/fapi/v3/klines", {
|
|
613
|
+
symbol: this._toApi(symbol), interval,
|
|
614
|
+
startTime: String(from), endTime: String(endTime), limit: String(ASTER_KLINE_PAGE),
|
|
615
|
+
});
|
|
616
|
+
if (!Array.isArray(data)) {
|
|
617
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster klines is not a list (${JSON.stringify(data).slice(0, 120)})`, { exchange: "aster" });
|
|
618
|
+
}
|
|
619
|
+
for (const k of data) {
|
|
620
|
+
const time = Number(k[0]);
|
|
621
|
+
const trades = Number(k[8]);
|
|
622
|
+
const numeric = (v) => (typeof v === "string" || typeof v === "number") && String(v).trim() !== "" && Number.isFinite(Number(v));
|
|
623
|
+
if (!Number.isFinite(time) || !Number.isFinite(trades) || !numeric(k[5]) || !numeric(k[7])) {
|
|
624
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster kline row without open time / volume / quote volume / trade count: ${JSON.stringify(k).slice(0, 120)}`, { exchange: "aster" });
|
|
625
|
+
}
|
|
626
|
+
out.push({ time, open: String(k[1]), high: String(k[2]), low: String(k[3]), close: String(k[4]), volume: String(k[5]), quoteVolume: String(k[7]), trades });
|
|
627
|
+
}
|
|
628
|
+
if (data.length < ASTER_KLINE_PAGE)
|
|
629
|
+
break;
|
|
630
|
+
const next = Number(data[data.length - 1][0]) + 1;
|
|
631
|
+
if (next <= from) {
|
|
632
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster klines page did not advance past ${from}`, { exchange: "aster" });
|
|
633
|
+
}
|
|
634
|
+
from = next;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
572
637
|
}
|
|
573
638
|
// ── Account ──
|
|
574
639
|
async getBalance() {
|
|
@@ -923,7 +988,7 @@ export class AsterAdapter {
|
|
|
923
988
|
}
|
|
924
989
|
// ── Trading ──
|
|
925
990
|
async marketOrder(symbol, side, size, opts) {
|
|
926
|
-
//
|
|
991
|
+
// Never native MARKET (no price cap) — an IOC LIMIT at the
|
|
927
992
|
// protection price (mark ± opts.slippage.bps, tick-aligned toward crossing, inside
|
|
928
993
|
// PRICE_FILTER / PERCENT_PRICE). newOrderRespType=RESULT makes the response carry the
|
|
929
994
|
// final IOC status (FILLED / EXPIRED) and executedQty.
|
|
@@ -1020,7 +1085,7 @@ export class AsterAdapter {
|
|
|
1020
1085
|
const clientOrderId = opts?.clientOrderId === undefined ? null : requireAsterClientOrderId(opts.clientOrderId);
|
|
1021
1086
|
let params;
|
|
1022
1087
|
try {
|
|
1023
|
-
// Common TIF names
|
|
1088
|
+
// Common TIF names: GTC / IOC / ALO→GTX (GTX accepted as alias); FOK unsupported.
|
|
1024
1089
|
const tif = asterTif(opts?.tif ?? "GTC");
|
|
1025
1090
|
// Price must already be on PRICE_FILTER.tickSize — validated, never rounded (INVALID_TICK).
|
|
1026
1091
|
const px = requireOnTick(price, (await this._symbolPriceFilters(apiSymbol)).tickSize);
|
|
@@ -1052,7 +1117,7 @@ export class AsterAdapter {
|
|
|
1052
1117
|
/**
|
|
1053
1118
|
* Native modify (PUT /fapi/v3/order, V3 futures docs "Modify Order"): LIMIT orders
|
|
1054
1119
|
* only; TIF (incl. GTX) and reduceOnly stay on the order. `size` is the NEW UNFILLED
|
|
1055
|
-
* quantity
|
|
1120
|
+
* quantity; the venue's `quantity` is the order total, so
|
|
1056
1121
|
* quantity = size + executedQty (read just before). A fill landing between the read
|
|
1057
1122
|
* and the PUT can only shrink the remainder, never over-size it. A venue rejection
|
|
1058
1123
|
* leaves the original order untouched (docs).
|
|
@@ -1705,6 +1770,11 @@ export class AsterAdapter {
|
|
|
1705
1770
|
if (!res.ok) {
|
|
1706
1771
|
const text = await res.text();
|
|
1707
1772
|
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1773
|
+
// -1121 BAD_SYMBOL ("Invalid symbol.", live on /fapi/v3/depth for an unlisted symbol): the
|
|
1774
|
+
// symbol does not exist — SYMBOL_NOT_FOUND, not an UNKNOWN 500.
|
|
1775
|
+
if (asterErrorCode(text) === -1121) {
|
|
1776
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Aster: ${clean} [${method} ${path}]`, { exchange: "aster", remediation: "List markets: perp -e aster market list" });
|
|
1777
|
+
}
|
|
1708
1778
|
throw new Error(`${method} ${path} failed (${res.status}): ${clean}`);
|
|
1709
1779
|
}
|
|
1710
1780
|
return res.json();
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `builder: { b: <address>, f: <tenths of a bp> }`; max 0.1% perps / 1% spot; status
|
|
22
22
|
* info `{ type: "maxBuilderFee", user, builder }`. HIP-3 dexes are not covered by the
|
|
23
23
|
* docs → never attached there.
|
|
24
|
-
* - pacifica https://pacifica.
|
|
24
|
+
* - pacifica https://docs.pacifica.fi/programs/builder-program.md
|
|
25
25
|
* approve_builder_code { builder_code, max_fee_rate } signed by the account; per-order
|
|
26
26
|
* `builder_code` on create_market / create / stop/create / positions/tpsl. TWAP
|
|
27
27
|
* (create_twap_order) does not document builder_code → never sent there.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `builder: { b: <address>, f: <tenths of a bp> }`; max 0.1% perps / 1% spot; status
|
|
22
22
|
* info `{ type: "maxBuilderFee", user, builder }`. HIP-3 dexes are not covered by the
|
|
23
23
|
* docs → never attached there.
|
|
24
|
-
* - pacifica https://pacifica.
|
|
24
|
+
* - pacifica https://docs.pacifica.fi/programs/builder-program.md
|
|
25
25
|
* approve_builder_code { builder_code, max_fee_rate } signed by the account; per-order
|
|
26
26
|
* `builder_code` on create_market / create / stop/create / positions/tpsl. TWAP
|
|
27
27
|
* (create_twap_order) does not document builder_code → never sent there.
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { PerpError, classifyError, isDefiniteOrderFailure, isTransportFailure, isUnknownOutcomeServerError } from "../errors.js";
|
|
51
51
|
import { isVenueId } from "./venue-types.js";
|
|
52
|
+
import { VARIATIONAL_READ_ONLY_REMEDIATION } from "./venues/variational.js";
|
|
52
53
|
export const BUILDER_FEE_CAPABILITIES = {
|
|
53
54
|
hyperliquid: {
|
|
54
55
|
approval: "master", identity: "address", perpCapBps: 10, spotCapBps: 100, hip3: false, testnet: true,
|
|
@@ -56,7 +57,7 @@ export const BUILDER_FEE_CAPABILITIES = {
|
|
|
56
57
|
},
|
|
57
58
|
pacifica: {
|
|
58
59
|
approval: "master", identity: "code", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: true,
|
|
59
|
-
feeStepBps: 0.01, feeKind: "max", docs: "https://pacifica.
|
|
60
|
+
feeStepBps: 0.01, feeKind: "max", docs: "https://docs.pacifica.fi/programs/builder-program.md",
|
|
60
61
|
},
|
|
61
62
|
aster: {
|
|
62
63
|
approval: "master", identity: "address", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: false,
|
|
@@ -73,11 +74,21 @@ export const BUILDER_FEE_CAPABILITIES = {
|
|
|
73
74
|
lighter: null,
|
|
74
75
|
"lighter-rh": null,
|
|
75
76
|
arcus: null,
|
|
77
|
+
risex: null,
|
|
78
|
+
txflow: null,
|
|
79
|
+
variational: null,
|
|
80
|
+
grvt: null,
|
|
81
|
+
extended: null,
|
|
76
82
|
};
|
|
77
83
|
/** Why a venue has no builder-fee support (shown in CAPABILITY_UNSUPPORTED). */
|
|
78
84
|
export const BUILDER_FEE_UNSUPPORTED = {
|
|
79
85
|
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
86
|
"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)",
|
|
87
|
+
risex: "not implemented yet in perp-cli (RISEx builder codes are planned; note the venue's \"bps\" field is parts-per-million)",
|
|
88
|
+
txflow: "TxFlow is integrated through an unofficial read-only API (market data only)",
|
|
89
|
+
variational: VARIATIONAL_READ_ONLY_REMEDIATION,
|
|
90
|
+
grvt: "GRVT is read-only (market data) in this version; trading is planned for a later release",
|
|
91
|
+
extended: "Extended is available for market data only in this version (trading is planned for a later release)",
|
|
81
92
|
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
93
|
};
|
|
83
94
|
export function builderFeeUnsupported(venue) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
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).
|
|
3
|
+
* that two markets on different venues are the same underlying.
|
|
5
4
|
*
|
|
6
5
|
* Rules by asset class:
|
|
7
6
|
* - crypto: ticker identity after the adapter strips its quote suffix, plus
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
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).
|
|
3
|
+
* that two markets on different venues are the same underlying.
|
|
5
4
|
*
|
|
6
5
|
* Rules by asset class:
|
|
7
6
|
* - crypto: ticker identity after the adapter strips its quote suffix, plus
|
|
@@ -47,19 +46,44 @@ export const EXPLICIT_CANONICAL = {
|
|
|
47
46
|
"hl:xyz|SILVER": "SILVER",
|
|
48
47
|
"qfex|GOLD": "GOLD",
|
|
49
48
|
"qfex|SILVER": "SILVER",
|
|
49
|
+
"risex|XAU": "GOLD",
|
|
50
|
+
"risex|XAG": "SILVER",
|
|
51
|
+
"txflow|XAU": "GOLD",
|
|
52
|
+
"txflow|XAG": "SILVER",
|
|
53
|
+
"variational|XAU": "GOLD",
|
|
54
|
+
"variational|XAG": "SILVER",
|
|
55
|
+
"grvt|XAU": "GOLD",
|
|
56
|
+
"grvt|XAG": "SILVER",
|
|
57
|
+
"extended|XAU": "GOLD",
|
|
58
|
+
"extended|XAG": "SILVER",
|
|
50
59
|
// Crude oil (WTI)
|
|
51
60
|
"ondo|WTI": "CRUDE_OIL_WTI",
|
|
52
61
|
"hl:xyz|CL": "CRUDE_OIL_WTI",
|
|
53
62
|
"hl:flx|OIL": "CRUDE_OIL_WTI",
|
|
54
63
|
// QFEX CL blends expiring futures; roll-week gaps are caught by the cross-venue price filter.
|
|
55
64
|
"qfex|CL": "CRUDE_OIL_WTI",
|
|
65
|
+
"risex|CL": "CRUDE_OIL_WTI",
|
|
66
|
+
// Brent crude
|
|
67
|
+
"risex|BZ": "CRUDE_OIL_BRENT",
|
|
68
|
+
"txflow|BZ": "CRUDE_OIL_BRENT",
|
|
69
|
+
"txflow|CL": "CRUDE_OIL_WTI",
|
|
70
|
+
"variational|BZ": "CRUDE_OIL_BRENT",
|
|
71
|
+
"variational|CL": "CRUDE_OIL_WTI",
|
|
72
|
+
"grvt|BZ": "CRUDE_OIL_BRENT",
|
|
73
|
+
"grvt|CL": "CRUDE_OIL_WTI",
|
|
74
|
+
"extended|WTI": "CRUDE_OIL_WTI",
|
|
75
|
+
"extended|XBR": "CRUDE_OIL_BRENT",
|
|
56
76
|
// US equity indices
|
|
57
77
|
"ondo|US500": "SPX",
|
|
58
78
|
"ondo|US100": "NDX",
|
|
59
79
|
"qfex|US500": "SPX",
|
|
60
80
|
"qfex|US100": "NDX",
|
|
81
|
+
// Extended SPX500m / TECH100m: S&P 500 / Nasdaq-100 index perps (uiName SPX-USD / NDX-USD).
|
|
82
|
+
"extended|SPX500M": "SPX",
|
|
83
|
+
"extended|TECH100M": "NDX",
|
|
61
84
|
// FX
|
|
62
85
|
"ondo|USDJPY": "USDJPY",
|
|
86
|
+
"extended|USDJPY": "USDJPY",
|
|
63
87
|
};
|
|
64
88
|
/** Pairs verified to be DIFFERENT products despite similar names. Never match. */
|
|
65
89
|
export const NEVER_MATCH = new Set([
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive (derive.xyz, formerly Lyra) v3 public JSON-RPC over HTTP — official API, no auth.
|
|
3
|
+
*
|
|
4
|
+
* - `POST <base>/public/<method>` with the params object as the body → `{id, result}` or
|
|
5
|
+
* `{id, error: {code, message, data}}` (docs.derive.xyz/error-codes.md; live 2026-10-18).
|
|
6
|
+
* - Errors are all PerpErrors, each with a remediation: transport failure → EXCHANGE_UNREACHABLE;
|
|
7
|
+
* HTTP 429 or RPC -32000..-32099 → RATE_LIMITED (retry hint from the RPC `data` "Retry after {ms} ms",
|
|
8
|
+
* else the `retry-after` header); any other RPC error → EXCHANGE_ERROR. A JSON-RPC -32602 is a
|
|
9
|
+
* Derive-side rejection too: user input is validated before any request, so it is never reported as
|
|
10
|
+
* the user's INVALID_PARAMS. Every RPC error carries `details.rpcCode` / `rpcData` so callers can
|
|
11
|
+
* classify the venue code (12001 instrument_not_found → SYMBOL_NOT_FOUND is done by the caller,
|
|
12
|
+
* which knows what was asked).
|
|
13
|
+
* - Public budget (docs rate-limits.md, mainnet reference): 5 TPS per IP, FIXED 5 s window of 25
|
|
14
|
+
* points, 1 point per request. The local bucket (burst 5, 4/s) allows at most 5 + 4·5 = 25 in any 5 s.
|
|
15
|
+
*/
|
|
16
|
+
import { PerpError } from "../../errors.js";
|
|
17
|
+
import type { RateLimitPolicy } from "../venue.js";
|
|
18
|
+
import type { Network } from "../venue-types.js";
|
|
19
|
+
export declare const DERIVE_REST_URLS: Readonly<Record<Network, string>>;
|
|
20
|
+
export declare const DERIVE_HTTP_TIMEOUT_MS = 15000;
|
|
21
|
+
export declare const DERIVE_RATE_LIMIT: RateLimitPolicy;
|
|
22
|
+
/** Derive application code for an unknown instrument / currency (`instrument_not_found`). */
|
|
23
|
+
export declare const DERIVE_INSTRUMENT_NOT_FOUND = 12001;
|
|
24
|
+
/** JSON-RPC invalid params — Derive also uses it for "Ticker not found for instrument" (live 2026-10-18). */
|
|
25
|
+
export declare const DERIVE_INVALID_PARAMS = -32602;
|
|
26
|
+
/** Shape drift: an explicit EXCHANGE_ERROR, never a guessed value. */
|
|
27
|
+
export declare function deriveShapeError(what: string): PerpError;
|
|
28
|
+
/** The RPC error code a Derive call failed with, or null (not an RPC error). */
|
|
29
|
+
export declare function deriveRpcCode(e: unknown): number | null;
|
|
30
|
+
export declare class DeriveClient {
|
|
31
|
+
readonly network: Network;
|
|
32
|
+
private readonly baseUrl;
|
|
33
|
+
private readonly rateLimit;
|
|
34
|
+
constructor(network: Network, baseUrl?: string, rateLimit?: RateLimitPolicy);
|
|
35
|
+
/** `public/<method>`; resolves with `result`, every failure is a PerpError. */
|
|
36
|
+
call<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive (derive.xyz, formerly Lyra) v3 public JSON-RPC over HTTP — official API, no auth.
|
|
3
|
+
*
|
|
4
|
+
* - `POST <base>/public/<method>` with the params object as the body → `{id, result}` or
|
|
5
|
+
* `{id, error: {code, message, data}}` (docs.derive.xyz/error-codes.md; live 2026-10-18).
|
|
6
|
+
* - Errors are all PerpErrors, each with a remediation: transport failure → EXCHANGE_UNREACHABLE;
|
|
7
|
+
* HTTP 429 or RPC -32000..-32099 → RATE_LIMITED (retry hint from the RPC `data` "Retry after {ms} ms",
|
|
8
|
+
* else the `retry-after` header); any other RPC error → EXCHANGE_ERROR. A JSON-RPC -32602 is a
|
|
9
|
+
* Derive-side rejection too: user input is validated before any request, so it is never reported as
|
|
10
|
+
* the user's INVALID_PARAMS. Every RPC error carries `details.rpcCode` / `rpcData` so callers can
|
|
11
|
+
* classify the venue code (12001 instrument_not_found → SYMBOL_NOT_FOUND is done by the caller,
|
|
12
|
+
* which knows what was asked).
|
|
13
|
+
* - Public budget (docs rate-limits.md, mainnet reference): 5 TPS per IP, FIXED 5 s window of 25
|
|
14
|
+
* points, 1 point per request. The local bucket (burst 5, 4/s) allows at most 5 + 4·5 = 25 in any 5 s.
|
|
15
|
+
*/
|
|
16
|
+
import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
17
|
+
import { acquireRate, rateLimitedError } from "../rate-limiter.js";
|
|
18
|
+
export const DERIVE_REST_URLS = {
|
|
19
|
+
mainnet: "https://api.derive.xyz/v3",
|
|
20
|
+
testnet: "https://testnet.api.derive.xyz/v3",
|
|
21
|
+
};
|
|
22
|
+
export const DERIVE_HTTP_TIMEOUT_MS = 15_000;
|
|
23
|
+
export const DERIVE_RATE_LIMIT = { capacity: 5, refillPerSec: 4, weights: null };
|
|
24
|
+
/** Derive application code for an unknown instrument / currency (`instrument_not_found`). */
|
|
25
|
+
export const DERIVE_INSTRUMENT_NOT_FOUND = 12001;
|
|
26
|
+
/** JSON-RPC invalid params — Derive also uses it for "Ticker not found for instrument" (live 2026-10-18). */
|
|
27
|
+
export const DERIVE_INVALID_PARAMS = -32602;
|
|
28
|
+
const UNREACHABLE_REMEDIATION = "Check the network / api.derive.xyz status and retry";
|
|
29
|
+
const RATE_LIMIT_REMEDIATION = "Derive's public budget is 5 requests/s per IP (25 per 5 s window); wait and retry";
|
|
30
|
+
/** Shape drift: an explicit EXCHANGE_ERROR, never a guessed value. */
|
|
31
|
+
export function deriveShapeError(what) {
|
|
32
|
+
return new PerpError("EXCHANGE_ERROR", `Derive API returned an unexpected shape: ${what}`, {
|
|
33
|
+
exchange: "derive",
|
|
34
|
+
remediation: "Derive's v3 API response changed; perp-cli needs an update to read it",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** The RPC error code a Derive call failed with, or null (not an RPC error). */
|
|
38
|
+
export function deriveRpcCode(e) {
|
|
39
|
+
if (!(e instanceof PerpError))
|
|
40
|
+
return null;
|
|
41
|
+
const code = e.structured.details?.rpcCode;
|
|
42
|
+
return typeof code === "number" ? code : null;
|
|
43
|
+
}
|
|
44
|
+
export class DeriveClient {
|
|
45
|
+
network;
|
|
46
|
+
baseUrl;
|
|
47
|
+
rateLimit;
|
|
48
|
+
constructor(network, baseUrl = DERIVE_REST_URLS[network], rateLimit = DERIVE_RATE_LIMIT) {
|
|
49
|
+
this.network = network;
|
|
50
|
+
this.baseUrl = baseUrl;
|
|
51
|
+
this.rateLimit = rateLimit;
|
|
52
|
+
}
|
|
53
|
+
/** `public/<method>`; resolves with `result`, every failure is a PerpError. */
|
|
54
|
+
async call(method, params) {
|
|
55
|
+
await acquireRate(`derive:${this.baseUrl}`, this.rateLimit, method);
|
|
56
|
+
const what = `Derive public/${method}`;
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await fetch(`${this.baseUrl}/public/${method}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify(params),
|
|
63
|
+
signal: AbortSignal.timeout(DERIVE_HTTP_TIMEOUT_MS),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `${what}: ${extractErrorMessage(e)}`, { exchange: "derive", remediation: UNREACHABLE_REMEDIATION });
|
|
68
|
+
}
|
|
69
|
+
const text = await res.text().catch((e) => {
|
|
70
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `${what}: response body unreadable (${extractErrorMessage(e)})`, {
|
|
71
|
+
exchange: "derive", remediation: UNREACHABLE_REMEDIATION,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
let body;
|
|
75
|
+
try {
|
|
76
|
+
body = JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
body = undefined;
|
|
80
|
+
}
|
|
81
|
+
const rpcError = body !== null && typeof body === "object" ? body.error : undefined;
|
|
82
|
+
const hasRpcError = rpcError !== undefined && rpcError !== null;
|
|
83
|
+
if (res.status === 429) {
|
|
84
|
+
// The RPC body's "Retry after N ms" is preferred; the retry-after header only when the body has none.
|
|
85
|
+
const fromRpc = hasRpcError ? retryAfterMsOf(rpcError) : null;
|
|
86
|
+
if (fromRpc !== null)
|
|
87
|
+
throw rateLimited(`${what}: HTTP 429`, fromRpc, rpcCodeOf(rpcError));
|
|
88
|
+
throw rateLimitedError("derive", res.headers.get("retry-after"), text);
|
|
89
|
+
}
|
|
90
|
+
if (hasRpcError)
|
|
91
|
+
throw rpcErrorOf(what, rpcError);
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new PerpError("EXCHANGE_ERROR", `${what}: HTTP ${res.status} ${text.slice(0, 200)}`, {
|
|
94
|
+
exchange: "derive", status: res.status, remediation: "Derive answered with an HTTP error; check api.derive.xyz status and retry",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (body === undefined)
|
|
98
|
+
throw deriveShapeError(`${method} returned a non-JSON body`);
|
|
99
|
+
if (body === null || typeof body !== "object" || !("result" in body))
|
|
100
|
+
throw deriveShapeError(`${method} returned no result`);
|
|
101
|
+
return body.result;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function rpcErrorOf(what, err) {
|
|
105
|
+
if (err === null || typeof err !== "object")
|
|
106
|
+
return deriveShapeError(`${what}: error is not an object`);
|
|
107
|
+
const { code, message, data } = err;
|
|
108
|
+
if (typeof code !== "number" || typeof message !== "string")
|
|
109
|
+
return deriveShapeError(`${what}: error without code / message`);
|
|
110
|
+
const detail = typeof data === "string" && data !== "" && data !== message ? `${message} (${data})` : message;
|
|
111
|
+
if (code <= -32000 && code >= -32099)
|
|
112
|
+
return rateLimited(`${what}: ${detail}`, retryAfterMsOf(err), code);
|
|
113
|
+
return new PerpError("EXCHANGE_ERROR", `${what}: request rejected by Derive: ${detail} [code ${code}]`, {
|
|
114
|
+
exchange: "derive", rpcCode: code, rpcData: typeof data === "string" ? data : null,
|
|
115
|
+
remediation: "Retry later; if it persists, Derive's API changed and perp-cli needs an update",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function rateLimited(message, retryAfterMs, rpcCode) {
|
|
119
|
+
return new PerpError("RATE_LIMITED", message, {
|
|
120
|
+
exchange: "derive",
|
|
121
|
+
...(rpcCode !== null ? { rpcCode } : {}),
|
|
122
|
+
...(retryAfterMs !== null ? { retryAfterMs } : {}),
|
|
123
|
+
remediation: RATE_LIMIT_REMEDIATION,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function rpcCodeOf(err) {
|
|
127
|
+
const code = err !== null && typeof err === "object" ? err.code : undefined;
|
|
128
|
+
return typeof code === "number" ? code : null;
|
|
129
|
+
}
|
|
130
|
+
/** "Retry after {ms} ms" from an RPC error's `data`, or null. */
|
|
131
|
+
function retryAfterMsOf(err) {
|
|
132
|
+
const data = err !== null && typeof err === "object" ? err.data : undefined;
|
|
133
|
+
const m = typeof data === "string" ? /Retry after (\d+) ?ms/i.exec(data) : null;
|
|
134
|
+
return m !== null ? Number(m[1]) : null;
|
|
135
|
+
}
|