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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus agent (API key) lifecycle — design §10.4.
|
|
3
|
+
*
|
|
4
|
+
* approve:
|
|
5
|
+
* 1. Resolve the master EVM signer from OWS (`masterOwsWallet`).
|
|
6
|
+
* 2. Create or reuse OWS wallet `agent-arcus-<master>` (empty passphrase, same
|
|
7
|
+
* pattern as Pacifica's `agent-pac-<master>`); its Solana account is the
|
|
8
|
+
* Ed25519 key. API key = hex(32-byte pubkey decoded from the base58 address).
|
|
9
|
+
* OWS `signMessage(…, "solana", …)` produces a raw Ed25519 signature over
|
|
10
|
+
* the given bytes (no prefix) — verified locally against tweetnacl.
|
|
11
|
+
* 3. Sign the `CreateApiKey` EIP-712 typed data with the master wallet.
|
|
12
|
+
* 4. POST /v1/createApiKey (no X-API-Key).
|
|
13
|
+
* 5. Confirm via GET /v1/apiKeys?address= (poll until the key is ACTIVE).
|
|
14
|
+
* 6. Return ArcusAgentMeta — persisting it is the caller's job.
|
|
15
|
+
*
|
|
16
|
+
* revoke: POST /v1/revokeApiKey signed by the same master wallet (EIP-712
|
|
17
|
+
* `RevokeApiKey`). The OWS agent wallet is left in place for the caller.
|
|
18
|
+
*/
|
|
19
|
+
import type { ArcusAgentMeta } from "../../settings.js";
|
|
20
|
+
import type { Network } from "../venue-types.js";
|
|
21
|
+
/** Arcus accepts validUntil between 1 and 180 days from server time. */
|
|
22
|
+
export declare const ARCUS_MAX_KEY_DAYS = 180;
|
|
23
|
+
export declare function arcusAgentWalletName(masterOwsWallet: string): string;
|
|
24
|
+
/** Base58 Solana address → 64-hex Ed25519 public key. */
|
|
25
|
+
export declare function ed25519PubHexFromBase58(address: string): string;
|
|
26
|
+
interface ApiKeyEntry {
|
|
27
|
+
apiKey: string;
|
|
28
|
+
address: string;
|
|
29
|
+
status: string;
|
|
30
|
+
accountIndex?: number;
|
|
31
|
+
allSubaccounts?: boolean;
|
|
32
|
+
validUntil: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function listArcusApiKeys(network: Network, address: string): Promise<ApiKeyEntry[]>;
|
|
35
|
+
export interface ApproveArcusAgentOpts {
|
|
36
|
+
/** User-facing agent name; also sent as the Arcus `apiWalletName` (1–64 chars). */
|
|
37
|
+
agentName: string;
|
|
38
|
+
masterOwsWallet: string;
|
|
39
|
+
passphrase: string | null;
|
|
40
|
+
/** 0–9 for one subaccount, 255 for all subaccounts. */
|
|
41
|
+
accountIndex: number;
|
|
42
|
+
network: Network;
|
|
43
|
+
/** Key lifetime in days, 1..180. */
|
|
44
|
+
validDays: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function approveArcusAgent(opts: ApproveArcusAgentOpts): Promise<ArcusAgentMeta>;
|
|
47
|
+
export interface RevokeArcusAgentOpts {
|
|
48
|
+
meta: ArcusAgentMeta;
|
|
49
|
+
masterOwsWallet: string;
|
|
50
|
+
passphrase: string | null;
|
|
51
|
+
network: Network;
|
|
52
|
+
}
|
|
53
|
+
/** Revoke the key on Arcus. Throws on rejection (a 404 means the key is not registered to this owner). */
|
|
54
|
+
export declare function revokeArcusAgent(opts: RevokeArcusAgentOpts): Promise<void>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus agent (API key) lifecycle — design §10.4.
|
|
3
|
+
*
|
|
4
|
+
* approve:
|
|
5
|
+
* 1. Resolve the master EVM signer from OWS (`masterOwsWallet`).
|
|
6
|
+
* 2. Create or reuse OWS wallet `agent-arcus-<master>` (empty passphrase, same
|
|
7
|
+
* pattern as Pacifica's `agent-pac-<master>`); its Solana account is the
|
|
8
|
+
* Ed25519 key. API key = hex(32-byte pubkey decoded from the base58 address).
|
|
9
|
+
* OWS `signMessage(…, "solana", …)` produces a raw Ed25519 signature over
|
|
10
|
+
* the given bytes (no prefix) — verified locally against tweetnacl.
|
|
11
|
+
* 3. Sign the `CreateApiKey` EIP-712 typed data with the master wallet.
|
|
12
|
+
* 4. POST /v1/createApiKey (no X-API-Key).
|
|
13
|
+
* 5. Confirm via GET /v1/apiKeys?address= (poll until the key is ACTIVE).
|
|
14
|
+
* 6. Return ArcusAgentMeta — persisting it is the caller's job.
|
|
15
|
+
*
|
|
16
|
+
* revoke: POST /v1/revokeApiKey signed by the same master wallet (EIP-712
|
|
17
|
+
* `RevokeApiKey`). The OWS agent wallet is left in place for the caller.
|
|
18
|
+
*/
|
|
19
|
+
import bs58 from "bs58";
|
|
20
|
+
import { PerpError } from "../../errors.js";
|
|
21
|
+
import { loadOws } from "../../signer/ows-loader.js";
|
|
22
|
+
import { OwsEvmSigner } from "../../signer/ows-evm.js";
|
|
23
|
+
import { networkConfig } from "../venue.js";
|
|
24
|
+
import { arcusVenue } from "../venues/arcus.js";
|
|
25
|
+
import { ArcusClient } from "./client.js";
|
|
26
|
+
import { createApiKeyTypedData, nowNs, revokeApiKeyTypedData, splitEvmSignature } from "./signing.js";
|
|
27
|
+
/** Arcus accepts validUntil between 1 and 180 days from server time. */
|
|
28
|
+
export const ARCUS_MAX_KEY_DAYS = 180;
|
|
29
|
+
const DAY_MS = 86_400_000;
|
|
30
|
+
const CONFIRM_ATTEMPTS = 10;
|
|
31
|
+
const CONFIRM_INTERVAL_MS = 1_000;
|
|
32
|
+
export function arcusAgentWalletName(masterOwsWallet) {
|
|
33
|
+
return `agent-arcus-${masterOwsWallet}`;
|
|
34
|
+
}
|
|
35
|
+
function signingChainId(network) {
|
|
36
|
+
const id = networkConfig(arcusVenue, network).signingChainId;
|
|
37
|
+
if (id === null)
|
|
38
|
+
throw new Error("arcus venue definition is missing signingChainId");
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
async function signTyped(signer, td) {
|
|
42
|
+
return splitEvmSignature(await signer.signTypedData(td.domain, td.types, td.message));
|
|
43
|
+
}
|
|
44
|
+
/** Base58 Solana address → 64-hex Ed25519 public key. */
|
|
45
|
+
export function ed25519PubHexFromBase58(address) {
|
|
46
|
+
const bytes = bs58.decode(address);
|
|
47
|
+
if (bytes.length !== 32) {
|
|
48
|
+
throw new PerpError("INVALID_PARAMS", `Expected a 32-byte Ed25519 public key, got ${bytes.length} bytes`);
|
|
49
|
+
}
|
|
50
|
+
return Buffer.from(bytes).toString("hex");
|
|
51
|
+
}
|
|
52
|
+
export async function listArcusApiKeys(network, address) {
|
|
53
|
+
const res = await new ArcusClient(network, null).get("/v1/apiKeys", "apiKeys", { address });
|
|
54
|
+
if (!res || !Array.isArray(res.apiKeys)) {
|
|
55
|
+
throw new PerpError("EXCHANGE_ERROR", "Arcus /v1/apiKeys: missing apiKeys array");
|
|
56
|
+
}
|
|
57
|
+
return res.apiKeys;
|
|
58
|
+
}
|
|
59
|
+
function tryDeleteWallet(walletName) {
|
|
60
|
+
try {
|
|
61
|
+
loadOws().deleteWallet(walletName);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
process.stderr.write(`[warn] failed to clean up orphan wallet "${walletName}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function approveArcusAgent(opts) {
|
|
70
|
+
const { agentName, masterOwsWallet, passphrase, accountIndex, network, validDays } = opts;
|
|
71
|
+
if (agentName.length === 0 || agentName.length > 64) {
|
|
72
|
+
throw new PerpError("INVALID_PARAMS", "Arcus agent name must be 1–64 characters");
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isInteger(validDays) || validDays < 1 || validDays > ARCUS_MAX_KEY_DAYS) {
|
|
75
|
+
throw new PerpError("INVALID_PARAMS", `Arcus key lifetime must be 1–${ARCUS_MAX_KEY_DAYS} days (got ${validDays})`);
|
|
76
|
+
}
|
|
77
|
+
if (!(Number.isInteger(accountIndex) && ((accountIndex >= 0 && accountIndex <= 9) || accountIndex === 255))) {
|
|
78
|
+
throw new PerpError("INVALID_PARAMS", `Arcus accountIndex must be 0–9 or 255 (got ${accountIndex})`);
|
|
79
|
+
}
|
|
80
|
+
// 1. master
|
|
81
|
+
const master = OwsEvmSigner.create(masterOwsWallet, passphrase ?? undefined);
|
|
82
|
+
const ownerAddress = master.getAddress().toLowerCase();
|
|
83
|
+
// 2. agent wallet (create or reuse)
|
|
84
|
+
const ows = loadOws();
|
|
85
|
+
const agentWalletName = arcusAgentWalletName(masterOwsWallet);
|
|
86
|
+
const existing = ows.listWallets().find((w) => w.name === agentWalletName);
|
|
87
|
+
const created = existing === undefined;
|
|
88
|
+
const wallet = existing ?? ows.createWallet(agentWalletName, "");
|
|
89
|
+
const sol = wallet.accounts.find((a) => a.chainId.startsWith("solana:"));
|
|
90
|
+
if (!sol) {
|
|
91
|
+
if (created)
|
|
92
|
+
tryDeleteWallet(agentWalletName);
|
|
93
|
+
throw new PerpError("INVALID_PARAMS", `Agent wallet "${agentWalletName}" has no Solana (Ed25519) account`, {
|
|
94
|
+
remediation: "Check OWS vault configuration",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const apiKeyHex = ed25519PubHexFromBase58(sol.address);
|
|
98
|
+
// 3. EIP-712 CreateApiKey
|
|
99
|
+
const validUntilMs = Date.now() + validDays * DAY_MS;
|
|
100
|
+
const nonce = nowNs().toString(); // Unix-ns string, single-use per (address, accountIndex)
|
|
101
|
+
const chainId = signingChainId(network);
|
|
102
|
+
const signature = await signTyped(master, createApiKeyTypedData({
|
|
103
|
+
chainId, apiWalletName: agentName, publicKeyHex: apiKeyHex, validUntilMs, accountIndex, nonce,
|
|
104
|
+
}));
|
|
105
|
+
// 4. register
|
|
106
|
+
const client = new ArcusClient(network, null);
|
|
107
|
+
try {
|
|
108
|
+
await client.postUnsigned("/v1/createApiKey", "createApiKey", {
|
|
109
|
+
address: ownerAddress,
|
|
110
|
+
publicKey: apiKeyHex,
|
|
111
|
+
apiWalletName: agentName,
|
|
112
|
+
accountIndex,
|
|
113
|
+
validUntil: validUntilMs,
|
|
114
|
+
nonce,
|
|
115
|
+
signature,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
+
if (err instanceof PerpError && err.structured.code === "ORDER_PENDING") {
|
|
121
|
+
// The request may have registered the key: deleting the wallet would orphan an active
|
|
122
|
+
// venue API key whose private key no longer exists anywhere. Keep it and say so.
|
|
123
|
+
throw new PerpError("ORDER_PENDING", `Arcus createApiKey outcome unknown: ${msg}`, {
|
|
124
|
+
exchange: "arcus",
|
|
125
|
+
cause: err.structured,
|
|
126
|
+
agentWalletName,
|
|
127
|
+
apiKeyHex,
|
|
128
|
+
remediation: `Do NOT re-run approve yet — the key ${apiKeyHex} may already be registered. The agent wallet "${agentWalletName}" was KEPT (it holds that key). ` +
|
|
129
|
+
`Check the account's API keys on Arcus (perp wallet agent list arcus, or the Arcus UI), then re-run: perp wallet agent approve arcus --master ${masterOwsWallet}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const cleanupOk = created ? tryDeleteWallet(agentWalletName) : true;
|
|
133
|
+
throw new PerpError("APPROVE_FAILED", `Arcus createApiKey failed: ${msg}`, {
|
|
134
|
+
cause: err instanceof PerpError ? err.structured : undefined,
|
|
135
|
+
remediation: cleanupOk
|
|
136
|
+
? `Fix the cause above and retry: perp wallet agent approve arcus --master ${masterOwsWallet}`
|
|
137
|
+
: `Delete the orphan OWS wallet "${agentWalletName}" manually, then retry`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// 5. confirm
|
|
141
|
+
for (let i = 0; i < CONFIRM_ATTEMPTS; i++) {
|
|
142
|
+
const keys = await listArcusApiKeys(network, ownerAddress);
|
|
143
|
+
const hit = keys.find(k => k.apiKey.toLowerCase() === apiKeyHex && k.status === "ACTIVE");
|
|
144
|
+
if (hit) {
|
|
145
|
+
return {
|
|
146
|
+
kind: "arcus",
|
|
147
|
+
agentName,
|
|
148
|
+
agentWalletName,
|
|
149
|
+
apiKeyHex,
|
|
150
|
+
accountIndex,
|
|
151
|
+
ownerAddress,
|
|
152
|
+
validUntil: new Date(validUntilMs).toISOString(),
|
|
153
|
+
network,
|
|
154
|
+
approvedAt: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
await new Promise(r => setTimeout(r, CONFIRM_INTERVAL_MS));
|
|
158
|
+
}
|
|
159
|
+
throw new PerpError("APPROVE_PARTIAL", "Arcus accepted createApiKey but the key is not listed as ACTIVE yet", {
|
|
160
|
+
remediation: `Check GET /v1/apiKeys?address=${ownerAddress} for key ${apiKeyHex}, then re-run the approve command (re-registering an owned key is allowed)`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Revoke the key on Arcus. Throws on rejection (a 404 means the key is not registered to this owner). */
|
|
164
|
+
export async function revokeArcusAgent(opts) {
|
|
165
|
+
const { meta, masterOwsWallet, passphrase, network } = opts;
|
|
166
|
+
const master = OwsEvmSigner.create(masterOwsWallet, passphrase ?? undefined);
|
|
167
|
+
if (master.getAddress().toLowerCase() !== meta.ownerAddress.toLowerCase()) {
|
|
168
|
+
throw new PerpError("INVALID_PARAMS", `Master wallet ${master.getAddress()} does not own Arcus key ${meta.apiKeyHex}`, {
|
|
169
|
+
remediation: `Use the OWS wallet for ${meta.ownerAddress}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const nonce = nowNs().toString(); // Unix-ns string, single-use per (address, accountIndex)
|
|
173
|
+
const signature = await signTyped(master, revokeApiKeyTypedData({
|
|
174
|
+
chainId: signingChainId(network),
|
|
175
|
+
apiWalletName: meta.agentName,
|
|
176
|
+
publicKeyHex: meta.apiKeyHex,
|
|
177
|
+
accountIndex: meta.accountIndex,
|
|
178
|
+
nonce,
|
|
179
|
+
}));
|
|
180
|
+
await new ArcusClient(network, null).postUnsigned("/v1/revokeApiKey", "revokeApiKey", {
|
|
181
|
+
address: meta.ownerAddress,
|
|
182
|
+
publicKey: meta.apiKeyHex,
|
|
183
|
+
apiWalletName: meta.agentName,
|
|
184
|
+
accountIndex: meta.accountIndex,
|
|
185
|
+
nonce,
|
|
186
|
+
signature,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus REST client — raw fetch, Ed25519 header auth, venue rate limiting,
|
|
3
|
+
* HTTP/venue errors → PerpError.
|
|
4
|
+
*
|
|
5
|
+
* Auth headers (protected writes): X-API-Key (64-hex pubkey), X-Timestamp
|
|
6
|
+
* (Unix ns, ±30 s), X-Signature (128-hex Ed25519). All reads are public and
|
|
7
|
+
* take `?address=` (account state on Arcus is readable by anyone).
|
|
8
|
+
*/
|
|
9
|
+
import { PerpError, type ErrorCode } from "../../errors.js";
|
|
10
|
+
import type { Network } from "../venue-types.js";
|
|
11
|
+
import { type Ed25519SignFn } from "./signing.js";
|
|
12
|
+
export interface ArcusAuth {
|
|
13
|
+
apiKeyHex: string;
|
|
14
|
+
sign: Ed25519SignFn;
|
|
15
|
+
/** Master EVM address that owns the key (sent as `?address=`). */
|
|
16
|
+
ownerAddress: string;
|
|
17
|
+
}
|
|
18
|
+
type Query = Record<string, string | number | boolean | null | undefined>;
|
|
19
|
+
export interface ArcusResponse<T> {
|
|
20
|
+
status: number;
|
|
21
|
+
data: T;
|
|
22
|
+
}
|
|
23
|
+
/** Map an Arcus rejectionReason (engine) to an error code. Unknown → EXCHANGE_ERROR. */
|
|
24
|
+
export declare function arcusRejectionCode(reason: string | null | undefined): ErrorCode;
|
|
25
|
+
/** PerpError for an engine rejectionReason; details carry `rejectionReason` for callers that branch on it. */
|
|
26
|
+
export declare function arcusRejectionError(reason: string | null | undefined, message: string, details: Record<string, unknown>): PerpError;
|
|
27
|
+
/** setLeverage rejection (HTTP 422 or a 200/REJECTED body) → PerpError. Unknown reason → EXCHANGE_ERROR. */
|
|
28
|
+
export declare function arcusLeverageRejectError(reason: string | null | undefined, message: string, details: Record<string, unknown>): PerpError;
|
|
29
|
+
/**
|
|
30
|
+
* HTTP 429 → RATE_LIMITED. Order writes carry `retryAfterMs` (exact) and
|
|
31
|
+
* `reason`; otherwise Retry-After (whole seconds). Docs: api-reference/rate-limits.
|
|
32
|
+
*/
|
|
33
|
+
export declare function arcusRateLimitedError(retryAfterHeader: string | null, text: string): PerpError;
|
|
34
|
+
export declare class ArcusClient {
|
|
35
|
+
readonly network: Network;
|
|
36
|
+
private readonly auth;
|
|
37
|
+
readonly baseUrl: string;
|
|
38
|
+
private readonly rateKey;
|
|
39
|
+
constructor(network: Network, auth: ArcusAuth | null);
|
|
40
|
+
get hasAuth(): boolean;
|
|
41
|
+
requireAuth(): ArcusAuth;
|
|
42
|
+
private acquire;
|
|
43
|
+
/** Callers acquire the rate-limit slot first (signed requests do so BEFORE taking their timestamp). */
|
|
44
|
+
private send;
|
|
45
|
+
/** Public GET. `endpoint` is the rate-limit weight key. */
|
|
46
|
+
get<T>(path: string, endpoint: string, query?: Query): Promise<T>;
|
|
47
|
+
/** Unsigned POST (EIP-712 wallet-signed bodies: createApiKey / revokeApiKey / withdraw). */
|
|
48
|
+
postUnsigned<T>(path: string, endpoint: string, body: Record<string, unknown>): Promise<ArcusResponse<T>>;
|
|
49
|
+
/**
|
|
50
|
+
* Scheme-1 POST. The rate-limit slot is acquired first; only then is the
|
|
51
|
+
* timestamp `ctNs` taken and handed to `build`, which returns the typed-payload
|
|
52
|
+
* signature and the pre-serialized body (canonicalJson — the body's int64 ns
|
|
53
|
+
* `timestamp` exceeds Number precision and must be emitted from a bigint).
|
|
54
|
+
* `ctNs` becomes X-Timestamp; `?address=` carries the master address.
|
|
55
|
+
*/
|
|
56
|
+
postScheme1<T>(path: string, endpoint: string, build: (ctNs: bigint) => Promise<{
|
|
57
|
+
bodyJson: string;
|
|
58
|
+
signatureHex: string;
|
|
59
|
+
}>): Promise<ArcusResponse<T>>;
|
|
60
|
+
/** Scheme-2 POST: signs `ts + action + canonicalJson(body)` and sends that exact canonical body. */
|
|
61
|
+
postScheme2<T>(action: string, body: Record<string, unknown>): Promise<ArcusResponse<T>>;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus REST client — raw fetch, Ed25519 header auth, venue rate limiting,
|
|
3
|
+
* HTTP/venue errors → PerpError.
|
|
4
|
+
*
|
|
5
|
+
* Auth headers (protected writes): X-API-Key (64-hex pubkey), X-Timestamp
|
|
6
|
+
* (Unix ns, ±30 s), X-Signature (128-hex Ed25519). All reads are public and
|
|
7
|
+
* take `?address=` (account state on Arcus is readable by anyone).
|
|
8
|
+
*/
|
|
9
|
+
import { PerpError, orderOutcomeUnknown } from "../../errors.js";
|
|
10
|
+
import { acquireRate, rateLimitedError } from "../rate-limiter.js";
|
|
11
|
+
import { arcusVenue } from "../venues/arcus.js";
|
|
12
|
+
import { canonicalJson, nowNs, scheme2Message, signUtf8Hex } from "./signing.js";
|
|
13
|
+
const TIMEOUT_MS = 15_000;
|
|
14
|
+
function restUrl(net) {
|
|
15
|
+
const cfg = arcusVenue.networks[net];
|
|
16
|
+
if (!cfg)
|
|
17
|
+
throw new PerpError("NOT_IMPLEMENTED", `Arcus has no ${net} environment`);
|
|
18
|
+
return cfg.restUrl;
|
|
19
|
+
}
|
|
20
|
+
function qs(q) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const [k, v] of Object.entries(q)) {
|
|
23
|
+
if (v === null || v === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
26
|
+
}
|
|
27
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
28
|
+
}
|
|
29
|
+
/** OrderRejectionReason (openapi, docs.arcus.xyz; MISSING_MARK_PRICE: testnet changelog v1.8.12). */
|
|
30
|
+
const REJECTION_CODES = {
|
|
31
|
+
UNDERCOLLATERALIZED: "MARGIN_INSUFFICIENT",
|
|
32
|
+
DUPLICATE_CLIENT_ID: "DUPLICATE_ORDER",
|
|
33
|
+
POSITION_TPSL_ALREADY_EXISTS: "DUPLICATE_ORDER",
|
|
34
|
+
ORDER_NOT_FOUND: "ORDER_NOT_FOUND",
|
|
35
|
+
ORDER_NOT_FOUND_FOR_MODIFY: "ORDER_NOT_FOUND",
|
|
36
|
+
POSITION_SIZE_CAP_EXCEEDED: "SIZE_TOO_LARGE",
|
|
37
|
+
OPEN_INTEREST_CAP_EXCEEDED: "SIZE_TOO_LARGE",
|
|
38
|
+
POST_ONLY_WOULD_CROSS: "ORDER_REJECTED",
|
|
39
|
+
IOC_CANCELED: "ORDER_REJECTED",
|
|
40
|
+
FOK_FAILED: "ORDER_REJECTED",
|
|
41
|
+
COULD_NOT_FILL: "ORDER_REJECTED",
|
|
42
|
+
FILL_WILL_EXCEED_TRADING_BOUND: "ORDER_REJECTED",
|
|
43
|
+
ORDER_WILL_TAKE_LIQUIDITY_DURING_MARKET_HALT: "ORDER_REJECTED",
|
|
44
|
+
REDUCE_ONLY_WOULD_INCREASE: "INVALID_PARAMS",
|
|
45
|
+
ENTRY_TPSL_CANNOT_BE_POSITION_TPSL: "INVALID_PARAMS",
|
|
46
|
+
SELF_TRADE: "RISK_VIOLATION",
|
|
47
|
+
OPEN_ORDER_CAP_EXCEEDED: "RISK_VIOLATION",
|
|
48
|
+
TOO_MANY_CLIENT_IDS: "RISK_VIOLATION",
|
|
49
|
+
MISSING_MARK_PRICE: "PRICE_STALE",
|
|
50
|
+
MODIFY_CHANGED_IMMUTABLE_FIELD: "INVALID_PARAMS",
|
|
51
|
+
MODIFY_ZERO_SIZE: "INVALID_PARAMS",
|
|
52
|
+
MODIFY_TPSL_NOT_SUPPORTED: "CAPABILITY_UNSUPPORTED",
|
|
53
|
+
MODIFY_SUPERSEDED_BY_CANCEL: "ORDER_REJECTED",
|
|
54
|
+
MODIFY_SIZE_ALREADY_FILLED: "ORDER_REJECTED",
|
|
55
|
+
MODIFY_WOULD_CROSS_OUTSIDE_RTH_TRADING_BOUNDARY: "ORDER_REJECTED",
|
|
56
|
+
};
|
|
57
|
+
const REJECTION_REMEDIATION = {
|
|
58
|
+
POST_ONLY_WOULD_CROSS: "The post-only (ALO) price would cross the book — move the price away from the touch or drop post-only",
|
|
59
|
+
REDUCE_ONLY_WOULD_INCREASE: "Reduce-only order would open or grow the position — check the side and position size",
|
|
60
|
+
POSITION_TPSL_ALREADY_EXISTS: "A position TP/SL of this kind already exists for the market — cancel it first (one TP and one SL per market)",
|
|
61
|
+
OPEN_ORDER_CAP_EXCEEDED: "Open-order cap reached — cancel resting orders first: perp trade cancel-all -e arcus",
|
|
62
|
+
TOO_MANY_CLIENT_IDS: "10,000 live client ids reached — cancel resting orders first",
|
|
63
|
+
SELF_TRADE: "The order would match your own resting order — cancel it or change the price",
|
|
64
|
+
MISSING_MARK_PRICE: "No live mark price for this market (oracle gap) — retry once pricing resumes",
|
|
65
|
+
MODIFY_TPSL_NOT_SUPPORTED: "TP/SL orders cannot be modified — cancel and place a new one",
|
|
66
|
+
MODIFY_SUPERSEDED_BY_CANCEL: "A pending cancel for this order won — the order is cancelled",
|
|
67
|
+
MODIFY_SIZE_ALREADY_FILLED: "The order was already filled up to the new size and has been cancelled",
|
|
68
|
+
};
|
|
69
|
+
/** setLeverage 422 rejectReason (SetLeverageRejectReason). */
|
|
70
|
+
const LEVERAGE_REJECT_CODES = {
|
|
71
|
+
UNDERCOLLATERALIZED: { code: "MARGIN_INSUFFICIENT", remediation: "Lowering leverage would under-margin open positions — add collateral or reduce positions first" },
|
|
72
|
+
HAS_OPEN_POSITION: { code: "INVALID_PARAMS", remediation: "Isolated mode needs a flat market — close the position, then switch to isolated" },
|
|
73
|
+
INVALID_LEVERAGE: { code: "INVALID_PARAMS", remediation: "Leverage must be within [1, market max] (perp market list -e arcus)" },
|
|
74
|
+
UNKNOWN_MARKET: { code: "SYMBOL_NOT_FOUND", remediation: "List markets with: perp market list -e arcus" },
|
|
75
|
+
};
|
|
76
|
+
/** Map an Arcus rejectionReason (engine) to an error code. Unknown → EXCHANGE_ERROR. */
|
|
77
|
+
export function arcusRejectionCode(reason) {
|
|
78
|
+
return (reason && REJECTION_CODES[reason]) || "EXCHANGE_ERROR";
|
|
79
|
+
}
|
|
80
|
+
/** PerpError for an engine rejectionReason; details carry `rejectionReason` for callers that branch on it. */
|
|
81
|
+
export function arcusRejectionError(reason, message, details) {
|
|
82
|
+
const remediation = reason ? REJECTION_REMEDIATION[reason] : undefined;
|
|
83
|
+
return new PerpError(arcusRejectionCode(reason), message, {
|
|
84
|
+
...details,
|
|
85
|
+
rejectionReason: reason ?? null,
|
|
86
|
+
...(remediation !== undefined ? { remediation } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** setLeverage rejection (HTTP 422 or a 200/REJECTED body) → PerpError. Unknown reason → EXCHANGE_ERROR. */
|
|
90
|
+
export function arcusLeverageRejectError(reason, message, details) {
|
|
91
|
+
const hit = reason ? LEVERAGE_REJECT_CODES[reason] : undefined;
|
|
92
|
+
return new PerpError(hit ? hit.code : "EXCHANGE_ERROR", message, {
|
|
93
|
+
...details,
|
|
94
|
+
rejectReason: reason ?? null,
|
|
95
|
+
...(hit ? { remediation: hit.remediation } : {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* HTTP 429 → RATE_LIMITED. Order writes carry `retryAfterMs` (exact) and
|
|
100
|
+
* `reason`; otherwise Retry-After (whole seconds). Docs: api-reference/rate-limits.
|
|
101
|
+
*/
|
|
102
|
+
export function arcusRateLimitedError(retryAfterHeader, text) {
|
|
103
|
+
let body = {};
|
|
104
|
+
try {
|
|
105
|
+
body = JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
catch { /* non-JSON 429: header only */ }
|
|
108
|
+
const base = rateLimitedError("arcus", retryAfterHeader, text);
|
|
109
|
+
if (typeof body.retryAfterMs !== "number" && body.reason === undefined)
|
|
110
|
+
return base;
|
|
111
|
+
const reason = body.reason ?? null;
|
|
112
|
+
const hint = reason === "account_empty" ? "The subaccount trading pool is exhausted (drip: 1 action / 10 s) — slow the order loop"
|
|
113
|
+
: reason === "account_partial" ? "The subaccount trading pool cannot cover this charge — send a smaller batch"
|
|
114
|
+
: reason === "ip" ? "Per-IP read weight is exhausted — reduce non-order traffic from this IP"
|
|
115
|
+
: base.structured.remediation;
|
|
116
|
+
return new PerpError("RATE_LIMITED", `arcus: rate limited (HTTP 429${reason ? `, ${reason}` : ""})`, {
|
|
117
|
+
...(typeof body.retryAfterMs === "number" ? { retryAfterMs: body.retryAfterMs } : base.structured.details?.retryAfterMs !== undefined ? { retryAfterMs: base.structured.details.retryAfterMs } : {}),
|
|
118
|
+
reason,
|
|
119
|
+
body: text.slice(0, 500),
|
|
120
|
+
remediation: hint,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function toPerpError(status, text, endpoint) {
|
|
124
|
+
let body = {};
|
|
125
|
+
try {
|
|
126
|
+
body = JSON.parse(text);
|
|
127
|
+
}
|
|
128
|
+
catch { /* non-JSON error page: message carries raw text */ }
|
|
129
|
+
const msg = body.error ?? text.slice(0, 200);
|
|
130
|
+
const details = { status, endpoint, body: text.slice(0, 500) };
|
|
131
|
+
const m = `Arcus ${endpoint}: ${msg}`;
|
|
132
|
+
const lower = msg.toLowerCase();
|
|
133
|
+
if (body.rejectionReason)
|
|
134
|
+
return arcusRejectionError(body.rejectionReason, m, details);
|
|
135
|
+
if (body.rejectReason)
|
|
136
|
+
return arcusLeverageRejectError(body.rejectReason, m, details);
|
|
137
|
+
if (body.errorType === "Tick")
|
|
138
|
+
return new PerpError("INVALID_TICK", m, details);
|
|
139
|
+
if (body.errorType === "ReduceOnly")
|
|
140
|
+
return new PerpError("INVALID_PARAMS", m, details);
|
|
141
|
+
if (status === 401 || body.errorType === "Unauthorized") {
|
|
142
|
+
return new PerpError("SIGNATURE_FAILED", m, {
|
|
143
|
+
...details,
|
|
144
|
+
remediation: "Check the system clock (±30 s) and that the Arcus API key is registered and not expired: perp wallet agent approve arcus",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (status === 403 || body.code === "GEO_RESTRICTED") {
|
|
148
|
+
return new PerpError("POLICY_DENIED", m, {
|
|
149
|
+
...details,
|
|
150
|
+
remediation: lower.includes("whitelist")
|
|
151
|
+
? "This address is not on the Arcus access whitelist — request access at https://arcus.xyz"
|
|
152
|
+
: "Arcus rejected the request for this address/region",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (status === 404) {
|
|
156
|
+
if (lower.includes("no activity")) {
|
|
157
|
+
return new PerpError("DEPOSIT_REQUIRED", m, { ...details, remediation: "Deposit USDG into the Arcus account first" });
|
|
158
|
+
}
|
|
159
|
+
// "order" first: an order-lookup 404 message may also mention the market.
|
|
160
|
+
if (lower.includes("order"))
|
|
161
|
+
return new PerpError("ORDER_NOT_FOUND", m, details);
|
|
162
|
+
if (lower.includes("market"))
|
|
163
|
+
return new PerpError("SYMBOL_NOT_FOUND", m, details);
|
|
164
|
+
return new PerpError("EXCHANGE_ERROR", m, details);
|
|
165
|
+
}
|
|
166
|
+
if (status === 501)
|
|
167
|
+
return new PerpError("NOT_IMPLEMENTED", m, details);
|
|
168
|
+
if (status === 409)
|
|
169
|
+
return new PerpError("DUPLICATE_ORDER", m, details);
|
|
170
|
+
if (status >= 400 && status < 500)
|
|
171
|
+
return new PerpError("INVALID_PARAMS", m, details);
|
|
172
|
+
return new PerpError("EXCHANGE_ERROR", m, details);
|
|
173
|
+
}
|
|
174
|
+
export class ArcusClient {
|
|
175
|
+
network;
|
|
176
|
+
auth;
|
|
177
|
+
baseUrl;
|
|
178
|
+
rateKey;
|
|
179
|
+
constructor(network, auth) {
|
|
180
|
+
this.network = network;
|
|
181
|
+
this.auth = auth;
|
|
182
|
+
this.baseUrl = restUrl(network);
|
|
183
|
+
this.rateKey = `arcus:${network}`;
|
|
184
|
+
}
|
|
185
|
+
get hasAuth() { return this.auth !== null; }
|
|
186
|
+
requireAuth() {
|
|
187
|
+
if (!this.auth) {
|
|
188
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "Arcus: no API key configured for signed requests", {
|
|
189
|
+
remediation: "Register an agent key: perp wallet agent approve arcus",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return this.auth;
|
|
193
|
+
}
|
|
194
|
+
async acquire(endpoint) {
|
|
195
|
+
await acquireRate(this.rateKey, arcusVenue.rateLimit, endpoint);
|
|
196
|
+
}
|
|
197
|
+
/** Callers acquire the rate-limit slot first (signed requests do so BEFORE taking their timestamp). */
|
|
198
|
+
async send(method, url, endpoint, headers, body) {
|
|
199
|
+
// Timeout covers fetch + body read. A GET failure is definite (TIMEOUT /
|
|
200
|
+
// EXCHANGE_UNREACHABLE); a POST failure may have reached the venue → outcome
|
|
201
|
+
// unknown (ORDER_PENDING, never resubmit — design §12.3).
|
|
202
|
+
const signal = AbortSignal.timeout(TIMEOUT_MS);
|
|
203
|
+
let res;
|
|
204
|
+
let text;
|
|
205
|
+
try {
|
|
206
|
+
res = await fetch(url, {
|
|
207
|
+
method,
|
|
208
|
+
headers: body !== null ? { "Content-Type": "application/json", ...headers } : headers,
|
|
209
|
+
body,
|
|
210
|
+
signal,
|
|
211
|
+
});
|
|
212
|
+
text = await res.text();
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const reason = signal.aborted ? `timed out after ${TIMEOUT_MS}ms` : (err instanceof Error ? err.message : String(err));
|
|
216
|
+
if (method === "POST") {
|
|
217
|
+
throw orderOutcomeUnknown("arcus", endpoint, new Error(reason), "Do NOT resubmit — the request may have been applied. Check open orders, positions and balances on arcus");
|
|
218
|
+
}
|
|
219
|
+
throw new PerpError(signal.aborted ? "TIMEOUT" : "EXCHANGE_UNREACHABLE", `Arcus ${endpoint}: ${reason}`, { exchange: "arcus" });
|
|
220
|
+
}
|
|
221
|
+
if (res.status === 429)
|
|
222
|
+
throw arcusRateLimitedError(res.headers.get("retry-after"), text);
|
|
223
|
+
const pendingRemediation = "Do NOT resubmit — the request may have been applied. Check open orders, positions and balances on arcus";
|
|
224
|
+
// A POST answered 5xx (other than 501 Not Implemented) may have been applied before the error.
|
|
225
|
+
if (method === "POST" && res.status >= 500 && res.status !== 501) {
|
|
226
|
+
throw orderOutcomeUnknown("arcus", endpoint, new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`), pendingRemediation);
|
|
227
|
+
}
|
|
228
|
+
if (!res.ok)
|
|
229
|
+
throw toPerpError(res.status, text, endpoint);
|
|
230
|
+
let data;
|
|
231
|
+
try {
|
|
232
|
+
data = text.length ? JSON.parse(text) : null;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// A 2xx POST with an unreadable body WAS accepted — what it did is unknown.
|
|
236
|
+
if (method === "POST")
|
|
237
|
+
throw orderOutcomeUnknown("arcus", endpoint, new Error(`unreadable response (HTTP ${res.status})`), pendingRemediation);
|
|
238
|
+
throw new PerpError("EXCHANGE_ERROR", `Arcus ${endpoint}: invalid JSON`, { body: text.slice(0, 500) });
|
|
239
|
+
}
|
|
240
|
+
return { status: res.status, data: data };
|
|
241
|
+
}
|
|
242
|
+
/** Public GET. `endpoint` is the rate-limit weight key. */
|
|
243
|
+
async get(path, endpoint, query = {}) {
|
|
244
|
+
await this.acquire(endpoint);
|
|
245
|
+
return (await this.send("GET", `${this.baseUrl}${path}${qs(query)}`, endpoint, {}, null)).data;
|
|
246
|
+
}
|
|
247
|
+
/** Unsigned POST (EIP-712 wallet-signed bodies: createApiKey / revokeApiKey / withdraw). */
|
|
248
|
+
async postUnsigned(path, endpoint, body) {
|
|
249
|
+
await this.acquire(endpoint);
|
|
250
|
+
return this.send("POST", `${this.baseUrl}${path}`, endpoint, {}, JSON.stringify(body));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Scheme-1 POST. The rate-limit slot is acquired first; only then is the
|
|
254
|
+
* timestamp `ctNs` taken and handed to `build`, which returns the typed-payload
|
|
255
|
+
* signature and the pre-serialized body (canonicalJson — the body's int64 ns
|
|
256
|
+
* `timestamp` exceeds Number precision and must be emitted from a bigint).
|
|
257
|
+
* `ctNs` becomes X-Timestamp; `?address=` carries the master address.
|
|
258
|
+
*/
|
|
259
|
+
async postScheme1(path, endpoint, build) {
|
|
260
|
+
const auth = this.requireAuth();
|
|
261
|
+
await this.acquire(endpoint);
|
|
262
|
+
const ctNs = nowNs();
|
|
263
|
+
const { bodyJson, signatureHex } = await build(ctNs);
|
|
264
|
+
const url = `${this.baseUrl}${path}${qs({ address: auth.ownerAddress })}`;
|
|
265
|
+
return this.send("POST", url, endpoint, {
|
|
266
|
+
"X-API-Key": auth.apiKeyHex,
|
|
267
|
+
"X-Timestamp": ctNs.toString(),
|
|
268
|
+
"X-Signature": signatureHex,
|
|
269
|
+
}, bodyJson);
|
|
270
|
+
}
|
|
271
|
+
/** Scheme-2 POST: signs `ts + action + canonicalJson(body)` and sends that exact canonical body. */
|
|
272
|
+
async postScheme2(action, body) {
|
|
273
|
+
const auth = this.requireAuth();
|
|
274
|
+
await this.acquire(action);
|
|
275
|
+
const ts = nowNs();
|
|
276
|
+
const canonical = canonicalJson(body);
|
|
277
|
+
const sig = await signUtf8Hex(auth.sign, scheme2Message(ts, action, canonical));
|
|
278
|
+
const url = `${this.baseUrl}/v1/${action}${qs({ address: auth.ownerAddress })}`;
|
|
279
|
+
return this.send("POST", url, action, {
|
|
280
|
+
"X-API-Key": auth.apiKeyHex,
|
|
281
|
+
"X-Timestamp": ts.toString(),
|
|
282
|
+
"X-Signature": sig,
|
|
283
|
+
}, canonical);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arcus public market data — GET /v1/markets (one call serves both markets
|
|
3
|
+
* and funding rates).
|
|
4
|
+
*
|
|
5
|
+
* Mapping notes:
|
|
6
|
+
* - symbol: `baseAsset` (plain ticker, e.g. "TSLA"); venueSymbol: display name
|
|
7
|
+
* ("TSLA-USD") which is also the `market` key for l2OrderBook/trades/candles.
|
|
8
|
+
* - tickSize / stepSize: the top-level fields. `tickTiers` (per-price-band
|
|
9
|
+
* placement grid) is NOT folded into VenueMarket — the signed integer price
|
|
10
|
+
* always divides by the top-level tickSize; tiers only restrict which limit
|
|
11
|
+
* prices are accepted. The adapter validates/aligns against the tier tick
|
|
12
|
+
* (see tickForPrice) using the raw market.
|
|
13
|
+
* - maxLeverage: floor(1 / initialMarginFraction) (regular-hours IMF).
|
|
14
|
+
* - assetClass from `category`: CRYPTO → crypto, EQUITIES → equity,
|
|
15
|
+
* FOREX → fx. COMMODITIES / INDICES listings on Arcus are ETFs (GLD, SLV,
|
|
16
|
+
* USO, CPER, SPY, QQQ …) → "etf" when `fullAssetName` names an ETF/Trust/
|
|
17
|
+
* Fund/Shares; otherwise commodity / index (canonical then comes only from the
|
|
18
|
+
* explicit table).
|
|
19
|
+
* - session: only for markets with `regularTradingHours` (24/7 crypto → null).
|
|
20
|
+
* underlyingOpen = !isOutsideRth; priceBand = off-hours trading bounds when
|
|
21
|
+
* published; marginMultiplier = offHoursIMF / IMF.
|
|
22
|
+
* - funding: hourly (`fundingRate` is the per-hour fraction last charged);
|
|
23
|
+
* `nextFundingAt` is Unix seconds.
|
|
24
|
+
* - tradable: status === "ONLINE".
|
|
25
|
+
*/
|
|
26
|
+
import type { AssetClass, VenueMarket } from "../venue-types.js";
|
|
27
|
+
import type { VenuePublicData } from "../venue.js";
|
|
28
|
+
import { ArcusClient } from "./client.js";
|
|
29
|
+
export interface ArcusTickTier {
|
|
30
|
+
upToPrice?: string;
|
|
31
|
+
tick: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ArcusRawMarket {
|
|
34
|
+
marketDisplayName: string;
|
|
35
|
+
fullAssetName?: string;
|
|
36
|
+
marketId: number;
|
|
37
|
+
status: string;
|
|
38
|
+
baseAsset: string;
|
|
39
|
+
quoteAsset: string;
|
|
40
|
+
tickSize: string;
|
|
41
|
+
stepSize: string;
|
|
42
|
+
tickTiers: ArcusTickTier[] | null;
|
|
43
|
+
minOrderNotional: string | null;
|
|
44
|
+
minOrderSize: string | null;
|
|
45
|
+
maxOrderSize: string | null;
|
|
46
|
+
oraclePrice?: string;
|
|
47
|
+
markPrice?: string;
|
|
48
|
+
fundingRate?: string;
|
|
49
|
+
nextFundingAt?: number;
|
|
50
|
+
volume24h?: string;
|
|
51
|
+
volume24hNotional?: string;
|
|
52
|
+
openInterest?: string;
|
|
53
|
+
initialMarginFraction?: string;
|
|
54
|
+
maintenanceMarginFraction?: string;
|
|
55
|
+
offHoursInitialMarginFraction?: string;
|
|
56
|
+
regularTradingHours: unknown | null;
|
|
57
|
+
isOutsideRth: boolean;
|
|
58
|
+
upperTradingBound: string | null;
|
|
59
|
+
lowerTradingBound: string | null;
|
|
60
|
+
category: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function fetchArcusRawMarkets(client: ArcusClient): Promise<ArcusRawMarket[]>;
|
|
63
|
+
export declare function arcusAssetClass(m: ArcusRawMarket): AssetClass;
|
|
64
|
+
export declare function toArcusVenueMarket(m: ArcusRawMarket): VenueMarket;
|
|
65
|
+
export declare const arcusPublicData: VenuePublicData;
|