perp-cli 0.12.17 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX API key import (design §14.5 as amended by §14.12 #9).
|
|
3
|
+
*
|
|
4
|
+
* Keys are created on the QFEX website (Developer Settings, 2FA) — there is no programmatic
|
|
5
|
+
* approval. The public key + secret are entered via hidden prompt / stdin only (never flags,
|
|
6
|
+
* never env). The secret is stored only in ~/.perp/qfex-agents/<publicKey>.json (AES-256-GCM,
|
|
7
|
+
* 0600; SSOT Rule #3); settings keep the QfexAgentMeta pointer.
|
|
8
|
+
*
|
|
9
|
+
* The import BINDS the key to one concrete account UUID (the primary account's id, or the
|
|
10
|
+
* given subaccount) — never "whatever the venue defaults to" — shows it, and stores nothing
|
|
11
|
+
* until the caller confirms it. Every check is a read; no order, no subscription to act on,
|
|
12
|
+
* and the banker (funding) host is not called (its GET /address issues an address).
|
|
13
|
+
*/
|
|
14
|
+
import type { QfexAgentMeta } from "../../settings.js";
|
|
15
|
+
import type { Network } from "../venue-types.js";
|
|
16
|
+
import type { QfexCredentials } from "./signing.js";
|
|
17
|
+
import type { RateLimitPolicy } from "../venue.js";
|
|
18
|
+
export declare const QFEX_AGENTS_DIR = "qfex-agents";
|
|
19
|
+
export declare const QFEX_IMPORT_REMEDIATION = "perp wallet agent import qfex";
|
|
20
|
+
/** Read + decrypt the stored key. Throws KEY_NOT_FOUND / SIGNATURE_FAILED. */
|
|
21
|
+
export declare function loadQfexCredentials(publicKey: string): QfexCredentials;
|
|
22
|
+
export interface QfexAccountBinding {
|
|
23
|
+
accountId: string;
|
|
24
|
+
isPrimaryAccount: boolean;
|
|
25
|
+
/** How the account was established (shown to the user and recorded as the verification method). */
|
|
26
|
+
method: string;
|
|
27
|
+
}
|
|
28
|
+
/** QFEX's answer for an account with no balance record yet (never funded). */
|
|
29
|
+
export declare function isUnfundedAccountError(e: unknown): boolean;
|
|
30
|
+
export interface QfexImportOptions {
|
|
31
|
+
agentName: string;
|
|
32
|
+
publicKey: string;
|
|
33
|
+
secret: string;
|
|
34
|
+
network: Network;
|
|
35
|
+
/** --account-id; null = the user's primary account. */
|
|
36
|
+
accountId: string | null;
|
|
37
|
+
endpoints: {
|
|
38
|
+
rest: string;
|
|
39
|
+
tradeWs: string;
|
|
40
|
+
banker: string;
|
|
41
|
+
};
|
|
42
|
+
rateLimit: RateLimitPolicy;
|
|
43
|
+
/** Shown the resolved account; returning false stores nothing. */
|
|
44
|
+
confirmAccount(binding: QfexAccountBinding): Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify a key against the venue with reads only, bind it to one account, get that binding
|
|
48
|
+
* confirmed, then store it. Nothing is written if any step fails or the confirmation is declined.
|
|
49
|
+
*/
|
|
50
|
+
export declare function importQfexAgent(opts: QfexImportOptions): Promise<{
|
|
51
|
+
meta: QfexAgentMeta;
|
|
52
|
+
binding: QfexAccountBinding;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Delete the local keystore for `publicKey`. Returns true if a file was removed. The key stays
|
|
56
|
+
* valid on the venue — revoke it in QFEX Developer Settings.
|
|
57
|
+
*/
|
|
58
|
+
export declare function revokeQfexAgentLocal(publicKey: string): boolean;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX API key import (design §14.5 as amended by §14.12 #9).
|
|
3
|
+
*
|
|
4
|
+
* Keys are created on the QFEX website (Developer Settings, 2FA) — there is no programmatic
|
|
5
|
+
* approval. The public key + secret are entered via hidden prompt / stdin only (never flags,
|
|
6
|
+
* never env). The secret is stored only in ~/.perp/qfex-agents/<publicKey>.json (AES-256-GCM,
|
|
7
|
+
* 0600; SSOT Rule #3); settings keep the QfexAgentMeta pointer.
|
|
8
|
+
*
|
|
9
|
+
* The import BINDS the key to one concrete account UUID (the primary account's id, or the
|
|
10
|
+
* given subaccount) — never "whatever the venue defaults to" — shows it, and stores nothing
|
|
11
|
+
* until the caller confirms it. Every check is a read; no order, no subscription to act on,
|
|
12
|
+
* and the banker (funding) host is not called (its GET /address issues an address).
|
|
13
|
+
*/
|
|
14
|
+
import { deleteVenueSecret, loadVenueSecret, saveVenueSecret } from "../../agent-wallet/venue-keystore.js";
|
|
15
|
+
import { PerpError } from "../../errors.js";
|
|
16
|
+
import { QfexClient } from "./client.js";
|
|
17
|
+
import { QFEX_IDLE_CLOSE_MS, QFEX_TIMEOUTS, QfexTradeSession } from "./ws-transport.js";
|
|
18
|
+
export const QFEX_AGENTS_DIR = "qfex-agents";
|
|
19
|
+
export const QFEX_IMPORT_REMEDIATION = "perp wallet agent import qfex";
|
|
20
|
+
/** The key is also the keystore file name and a URL query value → a conservative charset. */
|
|
21
|
+
const PUBLIC_KEY_RE = /^[A-Za-z0-9._-]{1,200}$/;
|
|
22
|
+
const SECRET_RE = /^\S{1,200}$/;
|
|
23
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
function assertCredentialFormat(publicKey, secret) {
|
|
25
|
+
// The qfex_pub_… / qfex_secret_… prefixes in the docs are examples; they are not enforced.
|
|
26
|
+
if (!PUBLIC_KEY_RE.test(publicKey)) {
|
|
27
|
+
throw new PerpError("INVALID_PARAMS", "QFEX public key must be 1-200 characters of letters, digits, '.', '_' or '-'", {
|
|
28
|
+
remediation: "Copy the public key exactly as shown in QFEX Developer Settings",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!SECRET_RE.test(secret)) {
|
|
32
|
+
throw new PerpError("INVALID_PARAMS", "QFEX secret must be 1-200 characters without whitespace — input not shown", {
|
|
33
|
+
remediation: "Copy the secret exactly as shown when the key was created (it is displayed once)",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Read + decrypt the stored key. Throws KEY_NOT_FOUND / SIGNATURE_FAILED. */
|
|
38
|
+
export function loadQfexCredentials(publicKey) {
|
|
39
|
+
const plaintext = loadVenueSecret(QFEX_AGENTS_DIR, publicKey, "", QFEX_IMPORT_REMEDIATION);
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(plaintext);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new PerpError("SIGNATURE_FAILED", `QFEX keystore for ${publicKey} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`, {
|
|
46
|
+
remediation: QFEX_IMPORT_REMEDIATION,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const r = parsed;
|
|
50
|
+
if (r === null || typeof r !== "object" || typeof r.publicKey !== "string" || typeof r.secret !== "string") {
|
|
51
|
+
throw new PerpError("SIGNATURE_FAILED", `QFEX keystore for ${publicKey} is missing publicKey/secret`, { remediation: QFEX_IMPORT_REMEDIATION });
|
|
52
|
+
}
|
|
53
|
+
if (r.publicKey !== publicKey) {
|
|
54
|
+
throw new PerpError("SIGNATURE_FAILED", `QFEX keystore for ${publicKey} holds a different public key`, { remediation: QFEX_IMPORT_REMEDIATION });
|
|
55
|
+
}
|
|
56
|
+
return { publicKey: r.publicKey, secret: r.secret };
|
|
57
|
+
}
|
|
58
|
+
function codeOf(e) {
|
|
59
|
+
return e instanceof PerpError ? e.structured.code : null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Permission-probing read; any failure other than the two below aborts the import.
|
|
63
|
+
* true = the read succeeded, false = the venue denied it (403), null = the permission cannot be
|
|
64
|
+
* told: an account that has never been funded answers GET /user/positions with HTTP 500
|
|
65
|
+
* "position balance not found" (live production 2026-10-05) — the key authenticated, but there
|
|
66
|
+
* is no balance record to read yet.
|
|
67
|
+
*/
|
|
68
|
+
async function probe(read) {
|
|
69
|
+
try {
|
|
70
|
+
await read();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
if (codeOf(e) === "POLICY_DENIED")
|
|
75
|
+
return false;
|
|
76
|
+
if (isUnfundedAccountError(e))
|
|
77
|
+
return null;
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** QFEX's answer for an account with no balance record yet (never funded). */
|
|
82
|
+
export function isUnfundedAccountError(e) {
|
|
83
|
+
return e instanceof PerpError && /position balance not found/i.test(e.message);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Verify a key against the venue with reads only, bind it to one account, get that binding
|
|
87
|
+
* confirmed, then store it. Nothing is written if any step fails or the confirmation is declined.
|
|
88
|
+
*/
|
|
89
|
+
export async function importQfexAgent(opts) {
|
|
90
|
+
const publicKey = opts.publicKey.trim();
|
|
91
|
+
const secret = opts.secret.trim();
|
|
92
|
+
assertCredentialFormat(publicKey, secret);
|
|
93
|
+
if (opts.accountId !== null && !UUID_RE.test(opts.accountId)) {
|
|
94
|
+
throw new PerpError("INVALID_PARAMS", `--account-id "${opts.accountId}" is not a UUID`, {
|
|
95
|
+
remediation: "Use the account id shown in the QFEX web app (Subaccounts)",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const credentials = { publicKey, secret };
|
|
99
|
+
const client = new QfexClient({ restUrl: opts.endpoints.rest, bankerUrl: opts.endpoints.banker, network: opts.network, credentials, accountId: null });
|
|
100
|
+
// 1. Which account? GET /user/subaccounts/balance lists the user's accounts with is_master.
|
|
101
|
+
let binding;
|
|
102
|
+
let rows;
|
|
103
|
+
try {
|
|
104
|
+
const body = await client.getAs("/user/subaccounts/balance", null, null);
|
|
105
|
+
if (!Array.isArray(body.accounts))
|
|
106
|
+
throw new PerpError("EXCHANGE_ERROR", "QFEX /user/subaccounts/balance: response has no accounts list");
|
|
107
|
+
rows = body.accounts;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (codeOf(e) !== "POLICY_DENIED")
|
|
111
|
+
throw e;
|
|
112
|
+
rows = null; // the key may not list accounts (e.g. scoped to one account)
|
|
113
|
+
}
|
|
114
|
+
if (rows !== null) {
|
|
115
|
+
const ids = rows.filter(r => typeof r.account_id === "string").map(r => ({ id: r.account_id.toLowerCase(), master: r.is_master === true }));
|
|
116
|
+
if (opts.accountId !== null) {
|
|
117
|
+
const hit = ids.find(r => r.id === opts.accountId.toLowerCase());
|
|
118
|
+
if (hit === undefined) {
|
|
119
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `QFEX ${opts.network} lists no account ${opts.accountId} for this key`, {
|
|
120
|
+
remediation: `Accounts visible to the key: ${ids.map(r => `${r.id}${r.master ? " (primary)" : ""}`).join(", ") || "none"}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
binding = { accountId: hit.id, isPrimaryAccount: hit.master, method: "GET /user/subaccounts/balance lists the account" };
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const masters = ids.filter(r => r.master);
|
|
127
|
+
if (masters.length !== 1) {
|
|
128
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX /user/subaccounts/balance lists ${masters.length} primary accounts; cannot tell which account the key trades`, {
|
|
129
|
+
remediation: "Pass the account explicitly: perp wallet agent import qfex --account-id <uuid>",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
binding = { accountId: masters[0].id, isPrimaryAccount: true, method: "GET /user/subaccounts/balance: the is_master account" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
if (opts.accountId === null) {
|
|
137
|
+
throw new PerpError("POLICY_DENIED", "This QFEX key cannot list accounts, so the account it trades cannot be resolved", {
|
|
138
|
+
remediation: "Pass the account the key is scoped to: perp wallet agent import qfex --account-id <uuid> (shown in the QFEX web app)",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
binding = { accountId: opts.accountId.toLowerCase(), isPrimaryAccount: false, method: "given --account-id; a signed read as that account succeeded (the key cannot list accounts, so primary/sub is unknown — treated as a subaccount)" };
|
|
142
|
+
}
|
|
143
|
+
// 2. Reads AS that account. 401 aborts (SIGNATURE_FAILED); 403 = that permission is missing.
|
|
144
|
+
const viewPositions = await probe(() => client.getAs("/user/positions", null, binding.accountId));
|
|
145
|
+
const viewOrders = await probe(() => client.getAs("/user/trade", { limit: 1 }, binding.accountId));
|
|
146
|
+
if (rows === null && viewPositions !== true) {
|
|
147
|
+
const positions = viewPositions === false ? "was denied positions" : "could not read positions (unknown: the account is not funded yet)";
|
|
148
|
+
throw new PerpError("POLICY_DENIED", `The key could not list accounts and ${positions} as ${binding.accountId}; nothing proves it can act on that account`, {
|
|
149
|
+
remediation: viewPositions === false
|
|
150
|
+
? "Check the account id and the key's scope / View permissions in QFEX Developer Settings"
|
|
151
|
+
: "Fund the account first (a never-funded account has no balance record to read), or import a key with All accounts access",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// 3. Trade WebSocket authentication with the account named explicitly. No command is sent.
|
|
155
|
+
const session = new QfexTradeSession({
|
|
156
|
+
url: opts.endpoints.tradeWs, network: opts.network, credentials, accountId: binding.accountId, builderCode: null,
|
|
157
|
+
rateLimit: opts.rateLimit, timeouts: QFEX_TIMEOUTS, idleCloseMs: QFEX_IDLE_CLOSE_MS,
|
|
158
|
+
});
|
|
159
|
+
await session.verifyAuth();
|
|
160
|
+
// 4. The user confirms the account BEFORE anything is stored.
|
|
161
|
+
if (!(await opts.confirmAccount(binding))) {
|
|
162
|
+
throw new PerpError("INVALID_PARAMS", `QFEX import cancelled — account ${binding.accountId} was not confirmed; nothing was stored`, {
|
|
163
|
+
remediation: "Re-run and confirm the account, or pass the intended one: perp wallet agent import qfex --account-id <uuid>",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const keystorePath = saveVenueSecret(QFEX_AGENTS_DIR, publicKey, JSON.stringify({ publicKey, secret }), "");
|
|
167
|
+
const meta = {
|
|
168
|
+
kind: "qfex",
|
|
169
|
+
agentName: opts.agentName,
|
|
170
|
+
publicKey,
|
|
171
|
+
keystorePath,
|
|
172
|
+
accountId: binding.accountId,
|
|
173
|
+
isPrimaryAccount: binding.isPrimaryAccount,
|
|
174
|
+
// Positions and balance come from the same endpoint. Execute / withdraw cannot be checked without acting.
|
|
175
|
+
permissions: { viewPositions, viewBalance: viewPositions, viewOrders, executeOrders: null, depositWithdraw: null },
|
|
176
|
+
network: opts.network,
|
|
177
|
+
approvedAt: new Date().toISOString(),
|
|
178
|
+
source: "imported",
|
|
179
|
+
};
|
|
180
|
+
return { meta, binding };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Delete the local keystore for `publicKey`. Returns true if a file was removed. The key stays
|
|
184
|
+
* valid on the venue — revoke it in QFEX Developer Settings.
|
|
185
|
+
*/
|
|
186
|
+
export function revokeQfexAgentLocal(publicKey) {
|
|
187
|
+
return deleteVenueSecret(QFEX_AGENTS_DIR, publicKey);
|
|
188
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX raw REST client (design §14.1, §14.4, §14.6): raw fetch + node:crypto HMAC, no SDK.
|
|
3
|
+
*
|
|
4
|
+
* Two hosts: the API (`api.qfex.*` — market data, account reads, leverage, subaccount
|
|
5
|
+
* transfer) and the banker (`banker.qfex.*` — deposit address, withdraw). Both take the same
|
|
6
|
+
* four auth headers (signing.ts). Errors are RFC 7807 problem+json.
|
|
7
|
+
*
|
|
8
|
+
* No retries, no fallbacks (SSOT Rule #2). A GET failure is definite (nothing changed). A POST
|
|
9
|
+
* that may have reached the venue (transport failure, timeout, 5xx) is an UNKNOWN outcome:
|
|
10
|
+
* ORDER_PENDING, never re-sent.
|
|
11
|
+
*
|
|
12
|
+
* Rate limit: QFEX documents only the WebSocket budget ("HTTP endpoints … may be subject to
|
|
13
|
+
* separate controls"). UNVERIFIED — a conservative 5 requests/s per process is applied here.
|
|
14
|
+
*/
|
|
15
|
+
import type { RateLimitPolicy } from "../venue.js";
|
|
16
|
+
import type { Network } from "../venue-types.js";
|
|
17
|
+
import { type QfexCredentials } from "./signing.js";
|
|
18
|
+
/** UNVERIFIED: no published REST limit — 5 req/s (see module header). */
|
|
19
|
+
export declare const QFEX_REST_RATE_LIMIT: RateLimitPolicy;
|
|
20
|
+
/** Per-request timeout (fetch + body read). */
|
|
21
|
+
export declare const QFEX_REQUEST_TIMEOUT_MS = 15000;
|
|
22
|
+
export type QfexQuery = Record<string, string | number | null>;
|
|
23
|
+
export interface QfexClientOptions {
|
|
24
|
+
restUrl: string;
|
|
25
|
+
bankerUrl: string;
|
|
26
|
+
network: Network;
|
|
27
|
+
/** null → public endpoints only. */
|
|
28
|
+
credentials: QfexCredentials | null;
|
|
29
|
+
/** Account the signed requests act as (x-qfex-requested-account-id); null only while a key is being imported. */
|
|
30
|
+
accountId: string | null;
|
|
31
|
+
}
|
|
32
|
+
export declare class QfexClient {
|
|
33
|
+
private readonly opts;
|
|
34
|
+
private readonly restUrl;
|
|
35
|
+
private readonly bankerUrl;
|
|
36
|
+
private readonly rateKey;
|
|
37
|
+
constructor(opts: QfexClientOptions);
|
|
38
|
+
get hasCredentials(): boolean;
|
|
39
|
+
/** Public GET (no auth headers). */
|
|
40
|
+
publicGet<T>(path: string, query: QfexQuery | null): Promise<T>;
|
|
41
|
+
/** Signed GET as the configured account. */
|
|
42
|
+
get<T>(path: string, query: QfexQuery | null): Promise<T>;
|
|
43
|
+
/** Signed GET as an explicit account (`null` = no account header: the key's primary account). Import-time checks only. */
|
|
44
|
+
getAs<T>(path: string, query: QfexQuery | null, accountId: string | null): Promise<T>;
|
|
45
|
+
/** Signed POST; returns the parsed body, or null for an empty one (204 / empty 200). */
|
|
46
|
+
post<T>(path: string, query: QfexQuery | null, body: unknown): Promise<T | null>;
|
|
47
|
+
bankerGet<T>(path: string, query: QfexQuery | null): Promise<T>;
|
|
48
|
+
/** Banker POST acts on the primary account (deposits / withdrawals are primary-only): no account header. */
|
|
49
|
+
bankerPost<T>(path: string, body: unknown): Promise<T | null>;
|
|
50
|
+
private request;
|
|
51
|
+
private unknownOutcome;
|
|
52
|
+
}
|
|
53
|
+
/** A required GET body: QFEX GET endpoints always answer with a JSON object. */
|
|
54
|
+
export declare function requireBody<T>(body: T | null, what: string): T;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX raw REST client (design §14.1, §14.4, §14.6): raw fetch + node:crypto HMAC, no SDK.
|
|
3
|
+
*
|
|
4
|
+
* Two hosts: the API (`api.qfex.*` — market data, account reads, leverage, subaccount
|
|
5
|
+
* transfer) and the banker (`banker.qfex.*` — deposit address, withdraw). Both take the same
|
|
6
|
+
* four auth headers (signing.ts). Errors are RFC 7807 problem+json.
|
|
7
|
+
*
|
|
8
|
+
* No retries, no fallbacks (SSOT Rule #2). A GET failure is definite (nothing changed). A POST
|
|
9
|
+
* that may have reached the venue (transport failure, timeout, 5xx) is an UNKNOWN outcome:
|
|
10
|
+
* ORDER_PENDING, never re-sent.
|
|
11
|
+
*
|
|
12
|
+
* Rate limit: QFEX documents only the WebSocket budget ("HTTP endpoints … may be subject to
|
|
13
|
+
* separate controls"). UNVERIFIED — a conservative 5 requests/s per process is applied here.
|
|
14
|
+
*/
|
|
15
|
+
import { PerpError, orderOutcomeUnknown } from "../../errors.js";
|
|
16
|
+
import { acquireRate } from "../rate-limiter.js";
|
|
17
|
+
import { qfexBankerError, qfexRestError } from "./errors.js";
|
|
18
|
+
import { qfexNonce, qfexRestHeaders } from "./signing.js";
|
|
19
|
+
/** UNVERIFIED: no published REST limit — 5 req/s (see module header). */
|
|
20
|
+
export const QFEX_REST_RATE_LIMIT = { capacity: 5, refillPerSec: 5, weights: null };
|
|
21
|
+
/** Per-request timeout (fetch + body read). */
|
|
22
|
+
export const QFEX_REQUEST_TIMEOUT_MS = 15_000;
|
|
23
|
+
export class QfexClient {
|
|
24
|
+
opts;
|
|
25
|
+
restUrl;
|
|
26
|
+
bankerUrl;
|
|
27
|
+
rateKey;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.opts = opts;
|
|
30
|
+
this.restUrl = opts.restUrl.replace(/\/+$/, "");
|
|
31
|
+
this.bankerUrl = opts.bankerUrl.replace(/\/+$/, "");
|
|
32
|
+
this.rateKey = `qfex:${opts.network}:rest`;
|
|
33
|
+
}
|
|
34
|
+
get hasCredentials() {
|
|
35
|
+
return this.opts.credentials !== null;
|
|
36
|
+
}
|
|
37
|
+
/** Public GET (no auth headers). */
|
|
38
|
+
async publicGet(path, query) {
|
|
39
|
+
return requireBody(await this.request("api", "GET", path, query, null, false, this.opts.accountId), `GET ${path}`);
|
|
40
|
+
}
|
|
41
|
+
/** Signed GET as the configured account. */
|
|
42
|
+
async get(path, query) {
|
|
43
|
+
return requireBody(await this.request("api", "GET", path, query, null, true, this.opts.accountId), `GET ${path}`);
|
|
44
|
+
}
|
|
45
|
+
/** Signed GET as an explicit account (`null` = no account header: the key's primary account). Import-time checks only. */
|
|
46
|
+
async getAs(path, query, accountId) {
|
|
47
|
+
return requireBody(await this.request("api", "GET", path, query, null, true, accountId), `GET ${path}`);
|
|
48
|
+
}
|
|
49
|
+
/** Signed POST; returns the parsed body, or null for an empty one (204 / empty 200). */
|
|
50
|
+
async post(path, query, body) {
|
|
51
|
+
return this.request("api", "POST", path, query, body, true, this.opts.accountId);
|
|
52
|
+
}
|
|
53
|
+
async bankerGet(path, query) {
|
|
54
|
+
return requireBody(await this.request("banker", "GET", path, query, null, true, null), `GET ${path}`);
|
|
55
|
+
}
|
|
56
|
+
/** Banker POST acts on the primary account (deposits / withdrawals are primary-only): no account header. */
|
|
57
|
+
async bankerPost(path, body) {
|
|
58
|
+
return this.request("banker", "POST", path, null, body, true, null);
|
|
59
|
+
}
|
|
60
|
+
async request(host, method, path, query, body, auth, accountId) {
|
|
61
|
+
let pathWithQuery = path;
|
|
62
|
+
if (query !== null) {
|
|
63
|
+
const qs = new URLSearchParams();
|
|
64
|
+
for (const [k, v] of Object.entries(query))
|
|
65
|
+
if (v !== null)
|
|
66
|
+
qs.append(k, String(v));
|
|
67
|
+
const s = qs.toString();
|
|
68
|
+
if (s !== "")
|
|
69
|
+
pathWithQuery = `${path}?${s}`;
|
|
70
|
+
}
|
|
71
|
+
const base = host === "api" ? this.restUrl : this.bankerUrl;
|
|
72
|
+
const bodyStr = body === null ? "" : JSON.stringify(body);
|
|
73
|
+
// The rate-limit wait happens BEFORE the timestamp is taken (±5 min window, and a queued
|
|
74
|
+
// request must not go out with a stale one).
|
|
75
|
+
await acquireRate(this.rateKey, QFEX_REST_RATE_LIMIT, path);
|
|
76
|
+
const headers = { Accept: "application/json" };
|
|
77
|
+
if (bodyStr !== "")
|
|
78
|
+
headers["Content-Type"] = "application/json";
|
|
79
|
+
if (auth) {
|
|
80
|
+
const creds = this.opts.credentials;
|
|
81
|
+
if (creds === null) {
|
|
82
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", `QFEX: ${method} ${path} requires an API key`, {
|
|
83
|
+
remediation: "Import a QFEX API key: perp wallet agent import qfex",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
Object.assign(headers, qfexRestHeaders(creds, accountId, Date.now(), qfexNonce()));
|
|
87
|
+
}
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), QFEX_REQUEST_TIMEOUT_MS);
|
|
90
|
+
let res;
|
|
91
|
+
let text;
|
|
92
|
+
try {
|
|
93
|
+
res = await fetch(`${base}${pathWithQuery}`, { method, headers, ...(bodyStr !== "" ? { body: bodyStr } : {}), signal: controller.signal });
|
|
94
|
+
text = await res.text();
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
const aborted = controller.signal.aborted;
|
|
98
|
+
const reason = aborted ? `timed out after ${QFEX_REQUEST_TIMEOUT_MS}ms` : `failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
99
|
+
if (method === "POST")
|
|
100
|
+
throw this.unknownOutcome(host, path, `request ${reason}`);
|
|
101
|
+
throw new PerpError(aborted ? "TIMEOUT" : "EXCHANGE_UNREACHABLE", `QFEX: ${method} ${path} ${reason}`, {
|
|
102
|
+
exchange: "qfex", remediation: `Check network connectivity to ${base}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
// A 5xx on a POST does not say whether the venue applied it.
|
|
110
|
+
if (method === "POST" && res.status >= 500)
|
|
111
|
+
throw this.unknownOutcome(host, path, `HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
112
|
+
throw host === "banker" ? qfexBankerError(method, path, res.status, text) : qfexRestError(method, path, res.status, text);
|
|
113
|
+
}
|
|
114
|
+
if (text.trim() === "")
|
|
115
|
+
return null;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(text);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
if (method === "POST")
|
|
121
|
+
throw this.unknownOutcome(host, path, `HTTP ${res.status} with a non-JSON body`);
|
|
122
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX: ${method} ${path} returned non-JSON (HTTP ${res.status})`, {
|
|
123
|
+
exchange: "qfex", httpStatus: res.status, body: text.slice(0, 300),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
unknownOutcome(host, path, reason) {
|
|
128
|
+
return orderOutcomeUnknown("qfex", `POST ${path}`, new Error(reason), host === "banker"
|
|
129
|
+
? "Do NOT resubmit — POST /withdraw is not idempotent and may have been applied. Check the QFEX funding history (web app) and: perp account balance -e qfex"
|
|
130
|
+
: "Do NOT resubmit — the request may have been applied. Check: perp account balance -e qfex / perp account positions -e qfex");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** A required GET body: QFEX GET endpoints always answer with a JSON object. */
|
|
134
|
+
export function requireBody(body, what) {
|
|
135
|
+
if (body === null)
|
|
136
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX: ${what} returned an empty body`, { exchange: "qfex" });
|
|
137
|
+
return body;
|
|
138
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX venue status / error → PerpError (design §14.3 error table, §14.12 #3, #8, #11).
|
|
3
|
+
*
|
|
4
|
+
* Three sources:
|
|
5
|
+
* - order_response.status engine verdict on one order (definite once correlated);
|
|
6
|
+
* - error frames `{err:{…}}`, `{type:"Err",…}`, `{type:"error",…}` (all documented);
|
|
7
|
+
* - REST problem+json api.qfex.* and banker.qfex.*.
|
|
8
|
+
* Nothing here retries. A RATE_LIMITED on a cancel is surfaced, never re-sent.
|
|
9
|
+
*/
|
|
10
|
+
import { PerpError } from "../../errors.js";
|
|
11
|
+
/** order_response.status values (AsyncAPI OrderResponse.status enum). */
|
|
12
|
+
export declare const QFEX_ORDER_STATUSES: readonly ["ACK", "EXPIRED", "IOC_CANCELLED", "INVALID_PRICE", "INVALID_QUANTITY", "NOT_FOUND", "IOC_PARTIALLY_FILLED", "USER_NOT_FOUND", "PERMISSION_DENIED", "FILLED", "MODIFIED", "CANCELLED", "CANCELLED_STP", "REJECTED", "NO_SUCH_ORDER", "INVALID_ORDER_TYPE", "BAD_SYMBOL", "PRICE_LESS_THAN_MIN_PRICE", "PRICE_GREATER_THAN_MAX_PRICE", "CANNOT_MODIFY_PARTIAL_FILL", "CANNOT_MODIFY_ALO_WOULD_CROSS", "FAILED_MARGIN_CHECK", "REJECTED_MUST_CHANGE_VALUE_MODIFY", "NO_CHANGE_NO_UPDATE", "INVALID_TICK_SIZE_PRECISION_PRICE", "INVALID_TICK_SIZE_PRECISION_QUANTITY", "QUANTITY_LESS_THAN_MIN_QUANTITY", "QUANTITY_GREATER_THAN_MAX_QUANTITY", "REJECTED_GREATER_THAN_MAX_PRICE_BAND", "REJECTED_LESS_THAN_MIN_PRICE_BAND", "INVALID_TIME_IN_FORCE", "REJECTED_WOULD_BREACH_MAX_POSITION", "REJECTED_WOULD_BREACH_MAX_NOTIONAL", "CANNOT_MODIFY_NO_SUCH_ORDER", "REJECTED_MARKET_CLOSED", "USER_IN_LIQUIDATION", "REJECTED_FAILED_TO_PROCESS", "INVALID_TAKE_PROFIT_PRICE", "INVALID_STOP_LOSS_PRICE", "INVALID_CLIENT_ORDER_ID", "RATE_LIMITED", "REJECTED_TOO_MANY_OPEN_ORDERS", "REJECTED_OPEN_INTEREST_LIMIT", "REJECTED_TOTAL_OPEN_INTEREST_LIMIT", "REJECTED_LESS_THAN_MIN_NOTIONAL"];
|
|
13
|
+
export type QfexStatusClass =
|
|
14
|
+
/** Received by the gateway; not yet proven resting or filled. */
|
|
15
|
+
"ack"
|
|
16
|
+
/** FILLED — full when quantity_remaining is 0, otherwise a partial fill. */
|
|
17
|
+
| "fill"
|
|
18
|
+
/** MODIFIED — the order now lives under a new order_id. */
|
|
19
|
+
| "modified"
|
|
20
|
+
/** The order is gone from the book: IOC_PARTIALLY_FILLED, IOC_CANCELLED, CANCELLED, CANCELLED_STP, EXPIRED. */
|
|
21
|
+
| "terminal"
|
|
22
|
+
/** The engine refused the request. */
|
|
23
|
+
| "reject"
|
|
24
|
+
/** A status this build does not know — never interpreted. */
|
|
25
|
+
| "unknown";
|
|
26
|
+
export declare function qfexStatusClass(status: string): QfexStatusClass;
|
|
27
|
+
/** "… retry after 200ms" / "… retry after 2 seconds" → ms; null when the message carries no hint. */
|
|
28
|
+
export declare function parseRetryAfterMs(message: string | null): number | null;
|
|
29
|
+
export interface QfexOrderContext {
|
|
30
|
+
/** What was being done, for the message ("add_order AAPL-USD"). */
|
|
31
|
+
what: string;
|
|
32
|
+
/** Price band of the market at validation time (attached to band rejections). */
|
|
33
|
+
band: {
|
|
34
|
+
lower: string;
|
|
35
|
+
upper: string;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
/** Error for a correlated order_response whose status is an engine rejection. */
|
|
39
|
+
export declare function qfexOrderStatusError(status: string, ctx: QfexOrderContext, frame: unknown): PerpError;
|
|
40
|
+
export declare const QFEX_ERR_CODES: readonly ["RateLimited", "InvalidJSONFormat", "AlreadyAuthenticated", "InvalidParameter", "PermissionDenied", "ServerError", "InvalidOrder", "InvalidOrderId", "KycRequired", "TncRequired"];
|
|
41
|
+
export interface QfexErrFrame {
|
|
42
|
+
errorCode: string;
|
|
43
|
+
message: string | null;
|
|
44
|
+
/** Echo of the request (`{type, params}`); null when absent or empty (`{}`), which carries no evidence. */
|
|
45
|
+
echo: {
|
|
46
|
+
type: string;
|
|
47
|
+
params: Record<string, unknown>;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Is an error frame, received after a MUTATING request was sent, proof that the request was
|
|
52
|
+
* NOT applied? Only when the venue echoed that very request AND the code is a refusal.
|
|
53
|
+
* `ServerError` ("safe to retry" in the docs) says nothing about whether the engine saw the
|
|
54
|
+
* order, and an error without an echo — including the documented `RateLimited` with
|
|
55
|
+
* `incoming_message: {}` — cannot be attributed to this request: both are unknown outcomes.
|
|
56
|
+
*/
|
|
57
|
+
export declare function qfexErrIsDefiniteRefusal(err: QfexErrFrame, echoMatches: boolean): boolean;
|
|
58
|
+
/** PerpError for an error frame that is a definite answer (reads, or a refusal per qfexErrIsDefiniteRefusal). */
|
|
59
|
+
export declare function qfexErrFrameError(err: QfexErrFrame, what: string): PerpError;
|
|
60
|
+
/** api.qfex.* (trading / account REST): 401 → SIGNATURE_FAILED, 403 → POLICY_DENIED, 429 → RATE_LIMITED, 5xx → EXCHANGE_ERROR. */
|
|
61
|
+
export declare function qfexRestError(method: string, path: string, httpStatus: number, body: string): PerpError;
|
|
62
|
+
/**
|
|
63
|
+
* banker.qfex.* (deposit address / withdraw), docs programmatic-funding "Errors":
|
|
64
|
+
* 400 invalid amount / address / network, or insufficient withdrawable balance → definite refusal;
|
|
65
|
+
* 401 missing / invalid authentication OR terms not accepted → by body;
|
|
66
|
+
* 403 key lacks "Deposit and withdraw", or the account cannot use funding;
|
|
67
|
+
* The official CLI README adds that withdrawals need 2FA and cannot be started from a CLI, so a
|
|
68
|
+
* refusal here is expected for many accounts.
|
|
69
|
+
*/
|
|
70
|
+
export declare function qfexBankerError(method: string, path: string, httpStatus: number, body: string): PerpError;
|