perp-cli 0.12.17 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +286 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +316 -337
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +219 -6
- package/dist/exchanges/aster.js +1023 -153
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +122 -0
- package/dist/exchanges/ondo/funds.js +290 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1383 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +34 -0
- package/dist/exchanges/venues/ondo.js +140 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +28 -0
- package/dist/exchanges/venues/qfex.js +132 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +142 -55
- package/skills/perp-cli/references/agent-operations.md +110 -56
- package/skills/perp-cli/references/commands.md +266 -71
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `perp wallet agent import <venue>` — register an agent wallet / API key that was CREATED ON THE
|
|
3
|
+
* EXCHANGE WEBSITE, so the master wallet does not have to be on this computer. The master is
|
|
4
|
+
* identified only by its public address.
|
|
5
|
+
*
|
|
6
|
+
* Every venue verifies the key against the venue BEFORE anything is stored, where the venue
|
|
7
|
+
* offers a way to (a mismatch / unknown key is refused). A venue without a read-only check is
|
|
8
|
+
* imported as explicitly "unverified" — never reported as verified.
|
|
9
|
+
*
|
|
10
|
+
* venue secret (hidden prompt / stdin) public inputs live check
|
|
11
|
+
* hyperliquid API wallet private key (EVM) [--master-address] info userRole(agent) = agent of master; extraAgents → validUntil
|
|
12
|
+
* aster API wallet (signer) private key (EVM) --master-address (user) signed GET /fapi/v3/balance (user=master, signer=agent)
|
|
13
|
+
* pacifica agent wallet private key (Solana) --master-address (account) none (bindings are listed only via master-signed /agent/list) → unverified
|
|
14
|
+
* lighter(-rh) API key private key (hex) --account-index --api-key-index account exists; slot holds a key; auth token signed by the key accepted by GET /accountActiveOrders
|
|
15
|
+
* arcus API key private key (Ed25519) --master-address [--account-index] derived public key listed ACTIVE by GET /v1/apiKeys?address=<owner>
|
|
16
|
+
* ondo API key id + secret — authenticated GET /v1/account (same as approve)
|
|
17
|
+
* qfex API public key + secret [--account-id <uuid>] signed reads as the resolved account + Trade WebSocket auth; the account UUID is shown and must be confirmed
|
|
18
|
+
*
|
|
19
|
+
* Storage (SSOT Rule #3): EVM / Solana / Ed25519 keys → OWS vault wallet
|
|
20
|
+
* `agent-<venue>-import-<agentName>` (empty passphrase, like approved agent wallets); Lighter-family
|
|
21
|
+
* keys → network-tagged lighter keystore; Ondo → ~/.perp/ondo-agents/; QFEX → ~/.perp/qfex-agents/. Never argv, never .env,
|
|
22
|
+
* never printed (errors are passed through redactSecrets).
|
|
23
|
+
*/
|
|
24
|
+
import chalk from "chalk";
|
|
25
|
+
import { PerpError, classifyError } from "../errors.js";
|
|
26
|
+
import { acquireLock, releaseLock, getAgent, setAgent, deleteAgent, listAgents, agentNetworkOf, agentSourceOf, getVenueAgent, setVenueAgent, deleteVenueAgent, } from "../agent-wallet/store.js";
|
|
27
|
+
import { getVenue, networkConfig } from "../exchanges/venue.js";
|
|
28
|
+
import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
|
|
29
|
+
import { loadOws } from "../signer/ows-loader.js";
|
|
30
|
+
import { defaultAgentName, isLighterFamilyVenue, parseExpiresIn, promptHidden, qfexWithdrawKeyWarning } from "./agent.js";
|
|
31
|
+
import { importOwsWallet, normalizeSolanaKey } from "./wallet.js";
|
|
32
|
+
/** Shown wherever an import is offered: what the imported key can and cannot do. */
|
|
33
|
+
export const IMPORT_SCOPE_NOTE = "Trading works with the imported key. Deposits, withdrawals, transfers, referral and agent approvals need the master wallet — do them on the exchange website, or add the master with `perp wallet import`.";
|
|
34
|
+
/**
|
|
35
|
+
* Printed on every import: an imported key is held in a local file protected only by file
|
|
36
|
+
* permissions (empty passphrase), and what it may do (withdraw, transfer) is decided on the
|
|
37
|
+
* exchange website when the key is created.
|
|
38
|
+
*/
|
|
39
|
+
export const IMPORT_KEY_WARNING = "The imported key is stored with an empty passphrase — it is protected by file permissions only. Its permissions (trade / withdraw) are set on the exchange website: create API keys WITHOUT withdraw permission.";
|
|
40
|
+
/** Default record name for an imported key (distinct from the approve default `perp-cli-*`). */
|
|
41
|
+
export function defaultImportAgentName(venue, network) {
|
|
42
|
+
return defaultAgentName(venue, network).replace(/^perp-cli-/, "web-");
|
|
43
|
+
}
|
|
44
|
+
/** OWS wallet holding an imported EVM / Solana / Ed25519 agent key. */
|
|
45
|
+
export function importedAgentWalletName(venue, agentName) {
|
|
46
|
+
const prefix = venue === "hyperliquid" ? "hl" : venue === "pacifica" ? "pac" : venue;
|
|
47
|
+
return `agent-${prefix}-import-${agentName}`;
|
|
48
|
+
}
|
|
49
|
+
// ── Secret input (hidden prompt / stdin only) ────────────────────────────────
|
|
50
|
+
function readPipedStdin() {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
process.stdin.setEncoding("utf-8");
|
|
54
|
+
process.stdin.on("data", (c) => chunks.push(typeof c === "string" ? c : c.toString("utf-8")));
|
|
55
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
56
|
+
process.stdin.on("error", reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Prompt label for the venue's secret. */
|
|
60
|
+
export function importSecretLabel(venue) {
|
|
61
|
+
switch (venue) {
|
|
62
|
+
case "hyperliquid": return "Hyperliquid API wallet private key";
|
|
63
|
+
case "aster": return "Aster API wallet (signer) private key";
|
|
64
|
+
case "pacifica": return "Pacifica agent wallet private key";
|
|
65
|
+
case "lighter": return "Lighter API key private key";
|
|
66
|
+
case "lighter-rh": return "Lighter RH API key private key";
|
|
67
|
+
case "arcus": return "Arcus API key private key (Ed25519)";
|
|
68
|
+
case "ondo": return "Ondo API secret";
|
|
69
|
+
case "qfex": return "QFEX API secret";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Number of stdin lines the venue's secret takes (ondo: key id + secret; qfex: public key + secret; others: the key).
|
|
74
|
+
* Used by `perp setup --non-interactive --import-agents` to split stdin.
|
|
75
|
+
*/
|
|
76
|
+
export function importSecretLineCount(venue) {
|
|
77
|
+
return venue === "ondo" || venue === "qfex" ? 2 : 1;
|
|
78
|
+
}
|
|
79
|
+
/** Turn the venue's stdin lines into an ImportSecret. */
|
|
80
|
+
export function importSecretFromLines(venue, lines) {
|
|
81
|
+
if (venue === "ondo")
|
|
82
|
+
return { kind: "ondo", keyId: lines[0], secret: lines[1] };
|
|
83
|
+
if (venue === "qfex")
|
|
84
|
+
return { kind: "qfex", publicKey: lines[0], secret: lines[1] };
|
|
85
|
+
return { kind: "key", key: lines[0] };
|
|
86
|
+
}
|
|
87
|
+
/** Read the secret: hidden TTY prompt, else piped stdin (ondo: 2 lines key id + secret; others: 1 line). */
|
|
88
|
+
export async function readImportSecret(venue) {
|
|
89
|
+
if (process.stdin.isTTY === true) {
|
|
90
|
+
if (venue === "ondo") {
|
|
91
|
+
const keyId = (await promptHidden("Ondo API key id (hidden): ")).trim();
|
|
92
|
+
const secret = (await promptHidden("Ondo API secret (hidden): ")).trim();
|
|
93
|
+
return { kind: "ondo", keyId, secret };
|
|
94
|
+
}
|
|
95
|
+
if (venue === "qfex") {
|
|
96
|
+
const publicKey = (await promptHidden("QFEX API public key (hidden): ")).trim();
|
|
97
|
+
const secret = (await promptHidden("QFEX API secret (hidden): ")).trim();
|
|
98
|
+
return { kind: "qfex", publicKey, secret };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "key", key: (await promptHidden(`${importSecretLabel(venue)} (hidden): `)).trim() };
|
|
101
|
+
}
|
|
102
|
+
const lines = (await readPipedStdin()).split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
|
103
|
+
const want = importSecretLineCount(venue);
|
|
104
|
+
if (lines.length !== want) {
|
|
105
|
+
throw new PerpError("INVALID_PARAMS", `Expected ${want} line${want === 1 ? "" : "s"} on stdin (${venue === "ondo" ? "key id, then secret" : venue === "qfex" ? "public key, then secret" : importSecretLabel(venue)}); got ${lines.length}`, {
|
|
106
|
+
remediation: venue === "ondo"
|
|
107
|
+
? "printf '%s\\n%s\\n' \"$KEY_ID\" \"$SECRET\" | perp wallet agent import ondo"
|
|
108
|
+
: venue === "qfex"
|
|
109
|
+
? "printf '%s\\n%s\\n' \"$PUBLIC_KEY\" \"$SECRET\" | perp wallet agent import qfex --account-id <uuid>"
|
|
110
|
+
: `pbpaste | perp wallet agent import ${venue} ... (or run in a terminal for a hidden prompt)`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return importSecretFromLines(venue, lines);
|
|
114
|
+
}
|
|
115
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
116
|
+
function secretsOf(s) {
|
|
117
|
+
// A QFEX public key is not a secret (it is in the WebSocket URL), but an empty one is still refused.
|
|
118
|
+
return s.kind === "key" ? [s.key] : s.kind === "ondo" ? [s.keyId, s.secret] : [s.publicKey, s.secret];
|
|
119
|
+
}
|
|
120
|
+
/** Every textual form a secret may take in an error: as given, without 0x, lower case, and the normalized Ed25519 seed. */
|
|
121
|
+
function secretForms(secrets) {
|
|
122
|
+
const out = new Set();
|
|
123
|
+
for (const raw of secrets) {
|
|
124
|
+
const t = raw.trim();
|
|
125
|
+
if (t.length === 0)
|
|
126
|
+
continue;
|
|
127
|
+
for (const v of [t, t.replace(/^0x/i, "")]) {
|
|
128
|
+
out.add(v);
|
|
129
|
+
out.add(v.toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
out.add(normalizeSolanaKey(t));
|
|
133
|
+
}
|
|
134
|
+
catch { /* not an Ed25519 key form */ }
|
|
135
|
+
}
|
|
136
|
+
return [...out].filter(v => v.length >= 8);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Strip key material from text (Rule #3): every form of the given secrets, plus any hex run of
|
|
140
|
+
* private-key length (≥ 64). Public addresses (40 hex / base58) stay readable.
|
|
141
|
+
*/
|
|
142
|
+
function scrub(text, forms) {
|
|
143
|
+
let out = text;
|
|
144
|
+
for (const f of forms)
|
|
145
|
+
out = out.split(f).join("[redacted]");
|
|
146
|
+
return out.replace(/(0x)?[0-9a-fA-F]{64,}/g, "[redacted]");
|
|
147
|
+
}
|
|
148
|
+
/** Re-throw any error with key material stripped from message and remediation. */
|
|
149
|
+
function redacted(err, secrets, venue) {
|
|
150
|
+
const e = classifyError(err, venue);
|
|
151
|
+
const forms = secretForms(secrets);
|
|
152
|
+
const details = { ...(e.details ?? {}), exchange: venue };
|
|
153
|
+
if (e.remediation !== undefined)
|
|
154
|
+
details.remediation = scrub(e.remediation, forms);
|
|
155
|
+
return new PerpError(e.code, scrub(e.message, forms), details);
|
|
156
|
+
}
|
|
157
|
+
/** Venues whose import cannot be checked against the venue (stored as "unverified"). */
|
|
158
|
+
export const UNVERIFIED_IMPORT_VENUES = ["pacifica"];
|
|
159
|
+
/**
|
|
160
|
+
* M3: a batch import (perp setup --import-agents) must not carry the same key for two venues
|
|
161
|
+
* (a mis-ordered paste). Compares every textual form (0x / case / Ed25519 seed). The error
|
|
162
|
+
* names venues only — never the key.
|
|
163
|
+
*/
|
|
164
|
+
export function assertDistinctImportSecrets(entries) {
|
|
165
|
+
const seen = new Map();
|
|
166
|
+
for (const [venue, secret] of entries) {
|
|
167
|
+
for (const f of new Set(secretForms(secretsOf(secret)))) {
|
|
168
|
+
const other = seen.get(f);
|
|
169
|
+
if (other !== undefined && other !== venue) {
|
|
170
|
+
throw new PerpError("INVALID_PARAMS", `The same key was given for ${other} and ${venue} — nothing was imported`, {
|
|
171
|
+
remediation: "Each venue needs its own key created on that venue's website; pipe one key line per --import-agents venue, in order",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
seen.set(f, venue);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function venueHint(venue, network) {
|
|
179
|
+
const n = network === "testnet" ? "-n testnet " : "";
|
|
180
|
+
return `Create the key on the ${getVenue(venue).displayName} ${network} website, then: perp ${n}wallet agent import ${venue}`;
|
|
181
|
+
}
|
|
182
|
+
/** H1: the master (account owner) key is never stored as an agent / API key. */
|
|
183
|
+
function masterKeyRefused(venue, network) {
|
|
184
|
+
return new PerpError("INVALID_PARAMS", `${getVenue(venue).displayName}: that is the master key — create an API wallet on the website`, {
|
|
185
|
+
remediation: `${venueHint(venue, network)}. The master key is never stored as an agent; to use the master on this computer: perp wallet import`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function evmFromKey(key, label) {
|
|
189
|
+
const { ethers } = await import("ethers");
|
|
190
|
+
const hex = key.trim().replace(/^0x/i, "");
|
|
191
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
192
|
+
throw new PerpError("INVALID_PARAMS", `${label} must be 32 bytes of hex (64 characters, optional 0x) — input not shown`);
|
|
193
|
+
}
|
|
194
|
+
const wallet = new ethers.Wallet(`0x${hex}`);
|
|
195
|
+
return { address: wallet.address, wallet };
|
|
196
|
+
}
|
|
197
|
+
async function requireEvmAddress(addr, what, venue) {
|
|
198
|
+
if (addr === null) {
|
|
199
|
+
throw new PerpError("INVALID_PARAMS", `${getVenue(venue).displayName} import needs the master (${what}) address`, {
|
|
200
|
+
remediation: `Pass --master-address 0x… (the wallet you log in with on the ${getVenue(venue).displayName} website)`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const { ethers } = await import("ethers");
|
|
204
|
+
if (!ethers.isAddress(addr))
|
|
205
|
+
throw new PerpError("INVALID_PARAMS", `--master-address "${addr}" is not an EVM address`);
|
|
206
|
+
return ethers.getAddress(addr);
|
|
207
|
+
}
|
|
208
|
+
async function ed25519FromKey(key, label) {
|
|
209
|
+
let seedHex;
|
|
210
|
+
try {
|
|
211
|
+
seedHex = normalizeSolanaKey(key);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new PerpError("INVALID_PARAMS", `${label} must be base58, hex, or a JSON byte array (32-byte seed or 64-byte keypair) — input not shown`);
|
|
215
|
+
}
|
|
216
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
217
|
+
const kp = Keypair.fromSeed(Uint8Array.from(Buffer.from(seedHex, "hex")));
|
|
218
|
+
return { seedHex, publicKey: kp.publicKey.toBytes() };
|
|
219
|
+
}
|
|
220
|
+
function owsWalletNames() {
|
|
221
|
+
return new Set(loadOws().listWallets().map((w) => w.name));
|
|
222
|
+
}
|
|
223
|
+
function resolveExpiry(fromVenue, expiresIn) {
|
|
224
|
+
if (fromVenue !== null)
|
|
225
|
+
return fromVenue;
|
|
226
|
+
if (expiresIn === null)
|
|
227
|
+
return null;
|
|
228
|
+
try {
|
|
229
|
+
return parseExpiresIn(expiresIn).toISOString();
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
throw new PerpError("INVALID_PARAMS", e instanceof Error ? e.message : String(e), { remediation: "Use: 30d, 90d, 180d, 1y, or ISO-8601 datetime" });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
236
|
+
/**
|
|
237
|
+
* Name checks shared by every venue. Returns the record being replaced (an imported record of the
|
|
238
|
+
* same venue / network / name, only with `replace`), or null.
|
|
239
|
+
*/
|
|
240
|
+
function checkName(venue, network, agentName, replace) {
|
|
241
|
+
if (!NAME_RE.test(agentName)) {
|
|
242
|
+
throw new PerpError("INVALID_PARAMS", `Agent name "${agentName}" must be 1-64 letters, digits, '.', '_' or '-'`);
|
|
243
|
+
}
|
|
244
|
+
const existing = venue === "ondo" || venue === "arcus" || venue === "qfex" ? getVenueAgent(venue, agentName) : getAgent(venue, agentName);
|
|
245
|
+
if (existing === null)
|
|
246
|
+
return null;
|
|
247
|
+
const existingNetwork = "kind" in existing ? existing.network : agentNetworkOf(venue, existing);
|
|
248
|
+
if (existingNetwork !== network) {
|
|
249
|
+
throw new PerpError("INVALID_PARAMS", `${venue} agent "${agentName}" is already registered on ${existingNetwork}`, {
|
|
250
|
+
remediation: `Choose another --agent-name for the ${network} key (default: ${defaultImportAgentName(venue, network)})`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (!replace) {
|
|
254
|
+
throw new PerpError("INVALID_PARAMS", `${venue} agent "${agentName}" already exists on ${network}`, {
|
|
255
|
+
remediation: "Pass --replace to replace this imported key, or choose another --agent-name",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (agentSourceOf(venue, existing) !== "imported") {
|
|
259
|
+
throw new PerpError("INVALID_PARAMS", `${venue} agent "${agentName}" was approved by this CLI — --replace only replaces imported keys`, {
|
|
260
|
+
remediation: `Revoke it first (perp wallet agent revoke ${venue} ${agentName}) or choose another --agent-name`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
/** Delete the local key + record of an imported agent (the key stays authorized on the venue). */
|
|
266
|
+
export async function deleteImportedAgentLocal(venue, network, meta) {
|
|
267
|
+
let keyRemoved = false;
|
|
268
|
+
if ("kind" in meta) {
|
|
269
|
+
if (meta.kind === "ondo") {
|
|
270
|
+
const { revokeOndoAgentLocal } = await import("../exchanges/ondo/agent.js");
|
|
271
|
+
keyRemoved = revokeOndoAgentLocal(meta.keyId);
|
|
272
|
+
deleteVenueAgent("ondo", meta.agentName);
|
|
273
|
+
}
|
|
274
|
+
else if (meta.kind === "qfex") {
|
|
275
|
+
const { revokeQfexAgentLocal } = await import("../exchanges/qfex/agent.js");
|
|
276
|
+
keyRemoved = revokeQfexAgentLocal(meta.publicKey);
|
|
277
|
+
deleteVenueAgent("qfex", meta.agentName);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
keyRemoved = deleteOwsWalletIfPresent(meta.agentWalletName);
|
|
281
|
+
deleteVenueAgent("arcus", meta.agentName);
|
|
282
|
+
}
|
|
283
|
+
return { keyRemoved };
|
|
284
|
+
}
|
|
285
|
+
if (isLighterFamilyVenue(venue)) {
|
|
286
|
+
if (typeof meta.accountIndex === "number" && typeof meta.apiKeyIndex === "number") {
|
|
287
|
+
const { deleteLighterFamilyKey } = await import("../agent-wallet/lighter-keystore.js");
|
|
288
|
+
keyRemoved = deleteLighterFamilyKey(venue, network, meta.accountIndex, meta.apiKeyIndex);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
keyRemoved = deleteOwsWalletIfPresent(meta.agentWalletName);
|
|
293
|
+
}
|
|
294
|
+
deleteAgent(venue, meta.agentName);
|
|
295
|
+
return { keyRemoved };
|
|
296
|
+
}
|
|
297
|
+
function deleteOwsWalletIfPresent(name) {
|
|
298
|
+
if (!name || !owsWalletNames().has(name))
|
|
299
|
+
return false;
|
|
300
|
+
loadOws().deleteWallet(name);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* OWS wallet name for a new imported key. While the record being replaced (--replace) still holds
|
|
305
|
+
* the standard name, the new key is staged under a fresh name, so the old key + record stay intact
|
|
306
|
+
* until the new key is verified and stored (commitOwsImport swaps them).
|
|
307
|
+
*/
|
|
308
|
+
function stagedImportWalletName(venue, agentName, replacing) {
|
|
309
|
+
const std = importedAgentWalletName(venue, agentName);
|
|
310
|
+
return replacing !== null && replacing.agentWalletName === std ? `${std}-${Date.now().toString(36)}` : std;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Swap in a verified + stored key: write the record, then delete the replaced record's key. If the
|
|
314
|
+
* record write fails, the new key is removed and the old record + key are untouched.
|
|
315
|
+
*/
|
|
316
|
+
function commitOwsImport(walletName, writeRecord, replacedWallet) {
|
|
317
|
+
try {
|
|
318
|
+
writeRecord();
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
deleteOwsWalletIfPresent(walletName);
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
if (replacedWallet !== null && replacedWallet !== walletName)
|
|
325
|
+
deleteOwsWalletIfPresent(replacedWallet);
|
|
326
|
+
}
|
|
327
|
+
/** Store an EVM / Solana / Ed25519 agent key in the OWS vault (empty passphrase, like approved agent wallets). */
|
|
328
|
+
async function storeOwsAgentKey(walletName, key, chain) {
|
|
329
|
+
if (owsWalletNames().has(walletName)) {
|
|
330
|
+
throw new PerpError("INVALID_PARAMS", `OWS wallet "${walletName}" already exists (no agent record uses it)`, {
|
|
331
|
+
remediation: `Remove it (perp wallet remove ${walletName}) or choose another --agent-name`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const w = await importOwsWallet(walletName, "", { kind: "single", key, chain });
|
|
335
|
+
const evm = w.accounts.find((a) => a.chainId.startsWith("eip155:"));
|
|
336
|
+
const sol = w.accounts.find((a) => a.chainId.startsWith("solana:"));
|
|
337
|
+
return { evm: evm ? evm.address : null, solana: sol ? sol.address : null };
|
|
338
|
+
}
|
|
339
|
+
function baseMeta(agentName, network, expiresAt) {
|
|
340
|
+
return {
|
|
341
|
+
agentName,
|
|
342
|
+
masterWalletName: "",
|
|
343
|
+
owsApiKeyId: "",
|
|
344
|
+
owsPolicyId: "",
|
|
345
|
+
// "" = expiry unknown (the venue does not report it and no --expires-in was given); list shows "unknown".
|
|
346
|
+
expiresAt: expiresAt ?? "",
|
|
347
|
+
approvedAt: new Date().toISOString(),
|
|
348
|
+
// Set on the website; not readable for every venue — perp trading is what the CLI uses it for.
|
|
349
|
+
// canWithdraw null = unknown (never assumed false or true).
|
|
350
|
+
permissions: { canPerpTrade: true, canSpotTrade: false, canWithdraw: null },
|
|
351
|
+
asterApprovalNonce: "",
|
|
352
|
+
status: "active",
|
|
353
|
+
network,
|
|
354
|
+
source: "imported",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function postJson(url, body, what) {
|
|
358
|
+
const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
359
|
+
const text = await res.text();
|
|
360
|
+
if (!res.ok)
|
|
361
|
+
throw new PerpError("EXCHANGE_ERROR", `${what} failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(text);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
throw new PerpError("EXCHANGE_ERROR", `${what} returned non-JSON: ${text.slice(0, 200)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ── Per-venue imports ────────────────────────────────────────────────────────
|
|
370
|
+
async function importHyperliquid(p, agentName, key, replacing) {
|
|
371
|
+
const { address: agentAddress } = await evmFromKey(key, "Hyperliquid API wallet private key");
|
|
372
|
+
if (p.masterAddress !== null && p.masterAddress.toLowerCase() === agentAddress.toLowerCase())
|
|
373
|
+
throw masterKeyRefused("hyperliquid", p.network);
|
|
374
|
+
const infoUrl = `${networkConfig(getVenue("hyperliquid"), p.network).restUrl}/info`;
|
|
375
|
+
// Official info endpoint: userRole of an API wallet = { role: "agent", data: { user: <master> } }.
|
|
376
|
+
const role = await postJson(infoUrl, { type: "userRole", user: agentAddress.toLowerCase() }, "Hyperliquid userRole");
|
|
377
|
+
// userRole "user" = the address is an account itself (its key is a master key, not an API wallet).
|
|
378
|
+
if (role?.role === "user")
|
|
379
|
+
throw masterKeyRefused("hyperliquid", p.network);
|
|
380
|
+
if (role?.role !== "agent" || typeof role.data?.user !== "string") {
|
|
381
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${agentAddress} is not an authorized API wallet on Hyperliquid ${p.network} (userRole: ${String(role?.role)})`, {
|
|
382
|
+
remediation: `${venueHint("hyperliquid", p.network)} — and authorize it there (More → API). A mainnet key is not valid on testnet and vice versa.`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const { ethers } = await import("ethers");
|
|
386
|
+
const master = ethers.getAddress(role.data.user);
|
|
387
|
+
if (master.toLowerCase() === agentAddress.toLowerCase())
|
|
388
|
+
throw masterKeyRefused("hyperliquid", p.network);
|
|
389
|
+
if (p.masterAddress !== null && p.masterAddress.toLowerCase() !== master.toLowerCase()) {
|
|
390
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `API wallet ${agentAddress} is authorized for ${master}, not ${p.masterAddress}`, {
|
|
391
|
+
remediation: "Pass the master address the key was created for, or omit --master-address",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const extra = await postJson(infoUrl, { type: "extraAgents", user: master.toLowerCase() }, "Hyperliquid extraAgents");
|
|
395
|
+
const entry = Array.isArray(extra)
|
|
396
|
+
? extra.find(a => typeof a.address === "string" && a.address.toLowerCase() === agentAddress.toLowerCase())
|
|
397
|
+
: undefined;
|
|
398
|
+
const validUntil = entry !== undefined && typeof entry.validUntil === "number" ? entry.validUntil : null;
|
|
399
|
+
if (validUntil !== null && validUntil < Date.now()) {
|
|
400
|
+
throw new PerpError("AGENT_EXPIRED", `Hyperliquid API wallet ${agentAddress} expired at ${new Date(validUntil).toISOString()}`, { remediation: venueHint("hyperliquid", p.network) });
|
|
401
|
+
}
|
|
402
|
+
const expiresAt = resolveExpiry(validUntil !== null ? new Date(validUntil).toISOString() : null, p.expiresIn);
|
|
403
|
+
const walletName = stagedImportWalletName("hyperliquid", agentName, replacing);
|
|
404
|
+
const stored = await storeOwsAgentKey(walletName, key, "evm");
|
|
405
|
+
if (stored.evm === null || stored.evm.toLowerCase() !== agentAddress.toLowerCase()) {
|
|
406
|
+
deleteOwsWalletIfPresent(walletName);
|
|
407
|
+
throw new PerpError("SIGNATURE_FAILED", `OWS stored a different address (${stored.evm}) than the key's ${agentAddress}; nothing was saved`);
|
|
408
|
+
}
|
|
409
|
+
commitOwsImport(walletName, () => setAgent("hyperliquid", { ...baseMeta(agentName, p.network, expiresAt), agentWalletName: walletName, agentEvmAddress: agentAddress, userEvmAddress: master }), replacing?.agentWalletName ?? null);
|
|
410
|
+
return {
|
|
411
|
+
venue: "hyperliquid", network: p.network, agentName, source: "imported", masterAddress: master, agentAddress, publicKey: null,
|
|
412
|
+
accountIndex: null, apiKeyIndex: null, accountId: null, expiresAt, storage: `OWS wallet ${walletName}`,
|
|
413
|
+
verification: { status: "verified", method: `Hyperliquid info userRole: ${agentAddress} is an agent of ${master}${validUntil !== null ? "; extraAgents validUntil" : ""}` },
|
|
414
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function importAster(p, agentName, key, replacing) {
|
|
418
|
+
const { address: agentAddress, wallet } = await evmFromKey(key, "Aster API wallet private key");
|
|
419
|
+
const master = await requireEvmAddress(p.masterAddress, "user", "aster");
|
|
420
|
+
if (master.toLowerCase() === agentAddress.toLowerCase())
|
|
421
|
+
throw masterKeyRefused("aster", p.network);
|
|
422
|
+
const expiresAt = resolveExpiry(null, p.expiresIn);
|
|
423
|
+
const meta = { ...baseMeta(agentName, p.network, expiresAt), agentWalletName: "", agentEvmAddress: agentAddress, userEvmAddress: master };
|
|
424
|
+
// Live check: a read-only USER_DATA request signed by the agent for user = master. Aster
|
|
425
|
+
// verifies that the signer is an API wallet of that user — an unbound key is refused.
|
|
426
|
+
const { AsterAdapter } = await import("../exchanges/aster.js");
|
|
427
|
+
const adapter = new AsterAdapter(undefined, p.network === "testnet");
|
|
428
|
+
adapter.setAgent(meta, {
|
|
429
|
+
getAddress: () => agentAddress,
|
|
430
|
+
async signTypedData(domain, types, message) {
|
|
431
|
+
const signature = await wallet.signTypedData(domain, types, message);
|
|
432
|
+
const sig = signature.slice(2);
|
|
433
|
+
return { signature, r: `0x${sig.slice(0, 64)}`, s: `0x${sig.slice(64, 128)}`, v: parseInt(sig.slice(128, 130), 16) };
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
try {
|
|
437
|
+
await adapter.signedRequest(networkConfig(getVenue("aster"), p.network).restUrl, "GET", "/fapi/v3/balance", {});
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
441
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `Aster ${p.network} refused a read signed by API wallet ${agentAddress} for user ${master}: ${msg}`, {
|
|
442
|
+
remediation: `${venueHint("aster", p.network)} (Pro API → API wallet) and pass the address you log in with as --master-address. Aster may also require a completed deposit before authenticated requests.`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const walletName = stagedImportWalletName("aster", agentName, replacing);
|
|
446
|
+
const stored = await storeOwsAgentKey(walletName, key, "evm");
|
|
447
|
+
if (stored.evm === null || stored.evm.toLowerCase() !== agentAddress.toLowerCase()) {
|
|
448
|
+
deleteOwsWalletIfPresent(walletName);
|
|
449
|
+
throw new PerpError("SIGNATURE_FAILED", `OWS stored a different address (${stored.evm}) than the key's ${agentAddress}; nothing was saved`);
|
|
450
|
+
}
|
|
451
|
+
commitOwsImport(walletName, () => setAgent("aster", { ...meta, agentWalletName: walletName }), replacing?.agentWalletName ?? null);
|
|
452
|
+
return {
|
|
453
|
+
venue: "aster", network: p.network, agentName, source: "imported", masterAddress: master, agentAddress, publicKey: null,
|
|
454
|
+
accountIndex: null, apiKeyIndex: null, accountId: null, expiresAt, storage: `OWS wallet ${walletName}`,
|
|
455
|
+
verification: { status: "verified", method: `signed GET /fapi/v3/balance accepted (user ${master}, signer ${agentAddress})` },
|
|
456
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function importPacifica(p, agentName, key, replacing) {
|
|
460
|
+
const { publicKey } = await ed25519FromKey(key, "Pacifica agent wallet private key");
|
|
461
|
+
const bs58 = (await import("bs58")).default;
|
|
462
|
+
const agentAddress = bs58.encode(publicKey);
|
|
463
|
+
if (p.masterAddress === null) {
|
|
464
|
+
throw new PerpError("INVALID_PARAMS", "Pacifica import needs the master (account) address", {
|
|
465
|
+
remediation: "Pass --master-address <Solana address you log in with on Pacifica>",
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
let masterBytes;
|
|
469
|
+
try {
|
|
470
|
+
masterBytes = bs58.decode(p.masterAddress);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
masterBytes = new Uint8Array();
|
|
474
|
+
}
|
|
475
|
+
if (masterBytes.length !== 32)
|
|
476
|
+
throw new PerpError("INVALID_PARAMS", `--master-address "${p.masterAddress}" is not a Solana address`);
|
|
477
|
+
if (p.masterAddress === agentAddress)
|
|
478
|
+
throw masterKeyRefused("pacifica", p.network);
|
|
479
|
+
const expiresAt = resolveExpiry(null, p.expiresIn);
|
|
480
|
+
const walletName = stagedImportWalletName("pacifica", agentName, replacing);
|
|
481
|
+
const stored = await storeOwsAgentKey(walletName, key, "solana");
|
|
482
|
+
if (stored.solana !== agentAddress) {
|
|
483
|
+
deleteOwsWalletIfPresent(walletName);
|
|
484
|
+
throw new PerpError("SIGNATURE_FAILED", `OWS stored a different address (${stored.solana}) than the key's ${agentAddress}; nothing was saved`);
|
|
485
|
+
}
|
|
486
|
+
const masterAddress = p.masterAddress;
|
|
487
|
+
commitOwsImport(walletName, () => setAgent("pacifica", {
|
|
488
|
+
...baseMeta(agentName, p.network, expiresAt),
|
|
489
|
+
agentWalletName: walletName,
|
|
490
|
+
agentEvmAddress: "0x0000000000000000000000000000000000000000",
|
|
491
|
+
userEvmAddress: "0x0000000000000000000000000000000000000000",
|
|
492
|
+
agentSolanaAddress: agentAddress,
|
|
493
|
+
userSolanaAddress: masterAddress,
|
|
494
|
+
}), replacing?.agentWalletName ?? null);
|
|
495
|
+
return {
|
|
496
|
+
venue: "pacifica", network: p.network, agentName, source: "imported", masterAddress: p.masterAddress, agentAddress, publicKey: null,
|
|
497
|
+
accountIndex: null, apiKeyIndex: null, accountId: null, expiresAt, storage: `OWS wallet ${walletName}`,
|
|
498
|
+
verification: {
|
|
499
|
+
status: "unverified",
|
|
500
|
+
method: "Pacifica lists agent bindings only through a master-signed request (POST /agent/list), and has no read-only agent-signed call — the binding is NOT checked. The first order fails if the key is not bound to this account.",
|
|
501
|
+
},
|
|
502
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async function importLighter(p, venue, agentName, key, replacing) {
|
|
506
|
+
const name = getVenue(venue).displayName;
|
|
507
|
+
const hex = key.trim().replace(/^0x/i, "");
|
|
508
|
+
if (!/^[0-9a-fA-F]{80}$/.test(hex)) {
|
|
509
|
+
throw new PerpError("INVALID_PARAMS", `${name} API key private key must be 40 bytes of hex (80 characters, optional 0x) — input not shown`);
|
|
510
|
+
}
|
|
511
|
+
const accountIndex = p.accountIndex;
|
|
512
|
+
const slot = p.apiKeyIndex;
|
|
513
|
+
if (accountIndex === null || !Number.isInteger(accountIndex) || accountIndex < 0) {
|
|
514
|
+
throw new PerpError("INVALID_PARAMS", `${name} import needs --account-index <n> (shown with the API key on the website)`);
|
|
515
|
+
}
|
|
516
|
+
if (slot === null || !Number.isInteger(slot) || slot < 0 || slot > 254) {
|
|
517
|
+
throw new PerpError("INVALID_PARAMS", `${name} import needs --api-key-index <0-254> (the key's slot, shown on the website)`);
|
|
518
|
+
}
|
|
519
|
+
if (venue === "lighter-rh" && LIGHTER_RH_RESERVED_KEY_INDEXES.includes(slot)) {
|
|
520
|
+
throw new PerpError("INVALID_PARAMS", `${name} API key index ${slot} is reserved (${LIGHTER_RH_RESERVED_KEY_INDEXES.join(", ")})`);
|
|
521
|
+
}
|
|
522
|
+
// Keystore files are per network+account+slot; never share a slot with another local record.
|
|
523
|
+
for (const { meta } of listAgents(venue)) {
|
|
524
|
+
if (meta.accountIndex === accountIndex && meta.apiKeyIndex === slot && meta.agentName !== replacing?.agentName) {
|
|
525
|
+
throw new PerpError("INVALID_PARAMS", `${name} slot ${slot} of account ${accountIndex} is already held by local ${agentNetworkOf(venue, meta)} agent "${meta.agentName}"`, {
|
|
526
|
+
remediation: `Remove that record first (perp${agentNetworkOf(venue, meta) === "testnet" ? " -n testnet" : ""} wallet agent revoke ${venue} ${meta.agentName} --local-only) or import another slot`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// 1) the account exists on this network → owner L1 address
|
|
531
|
+
const restUrl = networkConfig(getVenue(venue), p.network).restUrl;
|
|
532
|
+
const res = await fetch(`${restUrl}/api/v1/account?by=index&value=${accountIndex}`);
|
|
533
|
+
const text = await res.text();
|
|
534
|
+
let acct = null;
|
|
535
|
+
try {
|
|
536
|
+
acct = JSON.parse(text);
|
|
537
|
+
}
|
|
538
|
+
catch { /* handled below */ }
|
|
539
|
+
const row = res.ok && acct !== null && Array.isArray(acct.accounts) ? acct.accounts[0] : undefined;
|
|
540
|
+
if (row === undefined || typeof row.l1_address !== "string") {
|
|
541
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${name} ${p.network} has no account ${accountIndex} (HTTP ${res.status}: ${text.slice(0, 160)})`, {
|
|
542
|
+
remediation: `${venueHint(venue, p.network)} --account-index <your account index>`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const { ethers } = await import("ethers");
|
|
546
|
+
const owner = ethers.getAddress(row.l1_address);
|
|
547
|
+
if (p.masterAddress !== null && p.masterAddress.toLowerCase() !== owner.toLowerCase()) {
|
|
548
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${name} account ${accountIndex} belongs to ${owner}, not ${p.masterAddress}`);
|
|
549
|
+
}
|
|
550
|
+
// 2) the slot holds a key, and 3) the key signs an auth token the venue accepts for that slot.
|
|
551
|
+
const expiresAt = resolveExpiry(null, p.expiresIn);
|
|
552
|
+
const meta = {
|
|
553
|
+
...baseMeta(agentName, p.network, expiresAt), agentWalletName: "", agentEvmAddress: owner, userEvmAddress: owner,
|
|
554
|
+
accountIndex, apiKeyIndex: slot,
|
|
555
|
+
};
|
|
556
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
557
|
+
const adapter = LighterAdapter.forVenue(venue, "", p.network === "testnet");
|
|
558
|
+
adapter.setAgentSigner(meta, hex);
|
|
559
|
+
adapter.setAddress(owner);
|
|
560
|
+
await adapter.init();
|
|
561
|
+
const keys = await adapter.listApiKeys();
|
|
562
|
+
const onSlot = keys.find(k => k.apiKeyIndex === slot);
|
|
563
|
+
if (onSlot === undefined || /^(0x)?0*$/.test(onSlot.publicKey.trim())) {
|
|
564
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${name} account ${accountIndex} has no API key in slot ${slot} on ${p.network}`, {
|
|
565
|
+
remediation: `${venueHint(venue, p.network)} --account-index ${accountIndex} --api-key-index <the key's slot>`,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
await adapter.assertAgentKeyAccepted();
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `${name} did not accept the key for account ${accountIndex} slot ${slot}: ${e instanceof Error ? e.message : String(e)}`, {
|
|
573
|
+
remediation: "Check that --api-key-index is the slot the website shows for this key (a key that does not match the slot's public key is refused)",
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const publicKey = onSlot.publicKey.replace(/^0x/i, "");
|
|
577
|
+
// Swap: the new key is stored (atomic write — the same slot is overwritten in place, as the venue
|
|
578
|
+
// slot now holds this key), then the record, then the replaced record's key if it was another slot.
|
|
579
|
+
const { saveLighterFamilyKey, deleteLighterFamilyKey } = await import("../agent-wallet/lighter-keystore.js");
|
|
580
|
+
const path = saveLighterFamilyKey(venue, p.network, accountIndex, slot, hex, "");
|
|
581
|
+
const old = replacing !== null && typeof replacing.accountIndex === "number" && typeof replacing.apiKeyIndex === "number"
|
|
582
|
+
? { account: replacing.accountIndex, slot: replacing.apiKeyIndex } : null;
|
|
583
|
+
const sameFile = old !== null && old.account === accountIndex && old.slot === slot;
|
|
584
|
+
try {
|
|
585
|
+
setAgent(venue, { ...meta, publicKey });
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
if (!sameFile)
|
|
589
|
+
deleteLighterFamilyKey(venue, p.network, accountIndex, slot);
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
592
|
+
if (old !== null && !sameFile)
|
|
593
|
+
deleteLighterFamilyKey(venue, p.network, old.account, old.slot);
|
|
594
|
+
return {
|
|
595
|
+
venue, network: p.network, agentName, source: "imported", masterAddress: owner, agentAddress: null, publicKey,
|
|
596
|
+
accountIndex, apiKeyIndex: slot, accountId: null, expiresAt, storage: path,
|
|
597
|
+
verification: { status: "verified", method: `account ${accountIndex} exists (owner ${owner}); slot ${slot} holds a key; an auth token signed by the imported key was accepted by GET /api/v1/accountActiveOrders` },
|
|
598
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function importArcus(p, agentName, key, replacing) {
|
|
602
|
+
const { seedHex, publicKey } = await ed25519FromKey(key, "Arcus API key private key");
|
|
603
|
+
const apiKeyHex = Buffer.from(publicKey).toString("hex");
|
|
604
|
+
const owner = (await requireEvmAddress(p.masterAddress, "owner", "arcus")).toLowerCase();
|
|
605
|
+
// The same 32 bytes pasted as the "API key" would be the owner's EVM private key itself.
|
|
606
|
+
const { ethers } = await import("ethers");
|
|
607
|
+
let asEvm = null;
|
|
608
|
+
try {
|
|
609
|
+
asEvm = new ethers.Wallet(`0x${seedHex}`).address.toLowerCase();
|
|
610
|
+
}
|
|
611
|
+
catch { /* not a secp256k1 scalar → cannot be the owner's EVM key */ }
|
|
612
|
+
if (asEvm === owner)
|
|
613
|
+
throw masterKeyRefused("arcus", p.network);
|
|
614
|
+
const { listArcusApiKeys } = await import("../exchanges/arcus/agent.js");
|
|
615
|
+
const keys = await listArcusApiKeys(p.network, owner);
|
|
616
|
+
const hit = keys.find(k => k.apiKey.toLowerCase() === apiKeyHex);
|
|
617
|
+
if (hit === undefined || hit.status !== "ACTIVE") {
|
|
618
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `Arcus ${p.network} lists no ACTIVE API key ${apiKeyHex} for ${owner}${hit !== undefined ? ` (status ${hit.status})` : ""}`, {
|
|
619
|
+
remediation: `${venueHint("arcus", p.network)} --master-address <owner address>`,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const venueIndex = hit.allSubaccounts === true ? 255 : typeof hit.accountIndex === "number" ? hit.accountIndex : null;
|
|
623
|
+
if (venueIndex === null)
|
|
624
|
+
throw new PerpError("EXCHANGE_ERROR", `Arcus /v1/apiKeys entry for ${apiKeyHex} has no accountIndex`);
|
|
625
|
+
if (p.accountIndex !== null && p.accountIndex !== venueIndex) {
|
|
626
|
+
throw new PerpError("INVALID_PARAMS", `Arcus key ${apiKeyHex} is scoped to account index ${venueIndex}, not ${p.accountIndex}`);
|
|
627
|
+
}
|
|
628
|
+
const validUntil = typeof hit.validUntil === "number" && hit.validUntil > 0 ? new Date(hit.validUntil).toISOString() : null;
|
|
629
|
+
const expiresAt = resolveExpiry(validUntil, p.expiresIn);
|
|
630
|
+
const walletName = stagedImportWalletName("arcus", agentName, replacing);
|
|
631
|
+
const stored = await storeOwsAgentKey(walletName, key, "solana");
|
|
632
|
+
const bs58 = (await import("bs58")).default;
|
|
633
|
+
if (stored.solana === null || Buffer.from(bs58.decode(stored.solana)).toString("hex") !== apiKeyHex) {
|
|
634
|
+
deleteOwsWalletIfPresent(walletName);
|
|
635
|
+
throw new PerpError("SIGNATURE_FAILED", "OWS stored a different Ed25519 key than the one verified; nothing was saved");
|
|
636
|
+
}
|
|
637
|
+
const meta = {
|
|
638
|
+
kind: "arcus", agentName, agentWalletName: walletName, apiKeyHex, accountIndex: venueIndex, ownerAddress: owner,
|
|
639
|
+
validUntil: expiresAt ?? "", network: p.network, approvedAt: new Date().toISOString(), source: "imported",
|
|
640
|
+
};
|
|
641
|
+
commitOwsImport(walletName, () => setVenueAgent("arcus", meta), replacing?.agentWalletName ?? null);
|
|
642
|
+
return {
|
|
643
|
+
venue: "arcus", network: p.network, agentName, source: "imported", masterAddress: owner, agentAddress: null, publicKey: apiKeyHex,
|
|
644
|
+
accountIndex: venueIndex, apiKeyIndex: null, accountId: null, expiresAt, storage: `OWS wallet ${walletName}`,
|
|
645
|
+
verification: { status: "verified", method: `GET /v1/apiKeys?address=${owner} lists the key's public key as ACTIVE` },
|
|
646
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
async function importOndo(p, agentName, s, replacing) {
|
|
650
|
+
const { approveOndoAgent } = await import("../exchanges/ondo/agent.js");
|
|
651
|
+
// approveOndoAgent authenticates GET /v1/account with the key before it writes the keystore
|
|
652
|
+
// (keyed by key id). The replaced key's keystore is removed only after the new record is written.
|
|
653
|
+
const meta = await approveOndoAgent({ agentName, keyId: s.keyId, secret: s.secret, network: p.network });
|
|
654
|
+
const replacedKeyId = replacing !== null && replacing.keyId !== meta.keyId ? replacing.keyId : null;
|
|
655
|
+
const { revokeOndoAgentLocal } = await import("../exchanges/ondo/agent.js");
|
|
656
|
+
try {
|
|
657
|
+
setVenueAgent("ondo", { ...meta, source: "imported" });
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
if (replacing === null || replacedKeyId !== null)
|
|
661
|
+
revokeOndoAgentLocal(meta.keyId);
|
|
662
|
+
throw e;
|
|
663
|
+
}
|
|
664
|
+
if (replacedKeyId !== null)
|
|
665
|
+
revokeOndoAgentLocal(replacedKeyId);
|
|
666
|
+
return {
|
|
667
|
+
venue: "ondo", network: p.network, agentName, source: "imported", masterAddress: "", agentAddress: null, publicKey: null,
|
|
668
|
+
accountIndex: null, apiKeyIndex: null, accountId: null, expiresAt: null, storage: meta.keystorePath,
|
|
669
|
+
verification: { status: "verified", method: "authenticated GET /v1/account succeeded with the key (account open, perps / API keys enabled); key scopes are not readable with the key itself" },
|
|
670
|
+
warnings: [IMPORT_KEY_WARNING],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/** A probed QFEX read permission: null = it could not be told (a never-funded account has no balance record to read). */
|
|
674
|
+
export function qfexPermissionText(p) {
|
|
675
|
+
return p === true ? "ok" : p === false ? "denied" : "unknown (account not funded yet)";
|
|
676
|
+
}
|
|
677
|
+
async function importQfex(p, agentName, s, replacing) {
|
|
678
|
+
const confirm = p.confirmAccount;
|
|
679
|
+
if (confirm === null) {
|
|
680
|
+
throw new PerpError("INVALID_PARAMS", "QFEX import needs the account binding to be confirmed", {
|
|
681
|
+
remediation: "Run in a terminal (y/N prompt), or pass the account explicitly: perp wallet agent import qfex --account-id <uuid>",
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const { importQfexAgent, revokeQfexAgentLocal } = await import("../exchanges/qfex/agent.js");
|
|
685
|
+
const { QFEX_ENDPOINTS } = await import("../exchanges/venues/qfex.js");
|
|
686
|
+
// importQfexAgent verifies with reads + a Trade WebSocket auth, resolves the concrete account
|
|
687
|
+
// UUID, has it confirmed, and only then writes the keystore (keyed by public key). The replaced
|
|
688
|
+
// key's keystore is removed only after the new record is written.
|
|
689
|
+
const { meta, binding } = await importQfexAgent({
|
|
690
|
+
agentName, publicKey: s.publicKey, secret: s.secret, network: p.network, accountId: p.accountId,
|
|
691
|
+
endpoints: QFEX_ENDPOINTS[p.network], rateLimit: getVenue("qfex").rateLimit, confirmAccount: confirm,
|
|
692
|
+
});
|
|
693
|
+
const replacedKey = replacing !== null && replacing.publicKey !== meta.publicKey ? replacing.publicKey : null;
|
|
694
|
+
try {
|
|
695
|
+
setVenueAgent("qfex", meta);
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
if (replacing === null || replacedKey !== null)
|
|
699
|
+
revokeQfexAgentLocal(meta.publicKey);
|
|
700
|
+
throw e;
|
|
701
|
+
}
|
|
702
|
+
if (replacedKey !== null)
|
|
703
|
+
revokeQfexAgentLocal(replacedKey);
|
|
704
|
+
const perms = meta.permissions;
|
|
705
|
+
return {
|
|
706
|
+
venue: "qfex", network: p.network, agentName, source: "imported", masterAddress: "", agentAddress: null, publicKey: meta.publicKey,
|
|
707
|
+
accountIndex: null, apiKeyIndex: null, accountId: meta.accountId, expiresAt: null, storage: meta.keystorePath,
|
|
708
|
+
verification: {
|
|
709
|
+
status: "verified",
|
|
710
|
+
method: `account ${meta.accountId} (${meta.isPrimaryAccount ? "primary" : "subaccount"}) — ${binding.method}; signed reads as that account: positions/balance ${qfexPermissionText(perms.viewPositions)}, orders ${qfexPermissionText(perms.viewOrders)}; Trade WebSocket authentication accepted. Execute / withdraw permissions cannot be checked without acting`,
|
|
711
|
+
},
|
|
712
|
+
warnings: [
|
|
713
|
+
IMPORT_KEY_WARNING,
|
|
714
|
+
"QFEX withdrawals are authorized by the API key alone: keep a trading key WITHOUT the \"Deposit and withdraw\" permission, and import a separate key for funding if you need it.",
|
|
715
|
+
...((w => (w === null ? [] : [w]))(qfexWithdrawKeyWarning(meta))),
|
|
716
|
+
],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Import one website-created agent / API key. Verifies (where the venue allows) before storing;
|
|
721
|
+
* a refused verification stores nothing. Holds the venue's agent lock. Key material never
|
|
722
|
+
* appears in a thrown error.
|
|
723
|
+
*/
|
|
724
|
+
export async function importVenueAgent(p) {
|
|
725
|
+
const secrets = secretsOf(p.secret);
|
|
726
|
+
if (secrets.some(x => x.trim().length === 0))
|
|
727
|
+
throw new PerpError("INVALID_PARAMS", "Empty key — nothing was imported");
|
|
728
|
+
const agentName = p.agentName ?? defaultImportAgentName(p.venue, p.network);
|
|
729
|
+
networkConfig(getVenue(p.venue), p.network); // NOT_IMPLEMENTED when the venue has no such network
|
|
730
|
+
acquireLock(p.venue);
|
|
731
|
+
try {
|
|
732
|
+
const replacing = checkName(p.venue, p.network, agentName, p.replace);
|
|
733
|
+
if (p.venue === "ondo") {
|
|
734
|
+
if (p.secret.kind !== "ondo")
|
|
735
|
+
throw new PerpError("INVALID_PARAMS", "Ondo import needs the API key id and secret");
|
|
736
|
+
return await importOndo(p, agentName, p.secret, replacing);
|
|
737
|
+
}
|
|
738
|
+
if (p.venue === "qfex") {
|
|
739
|
+
if (p.secret.kind !== "qfex")
|
|
740
|
+
throw new PerpError("INVALID_PARAMS", "QFEX import needs the API public key and secret");
|
|
741
|
+
return await importQfex(p, agentName, p.secret, replacing);
|
|
742
|
+
}
|
|
743
|
+
if (p.secret.kind !== "key")
|
|
744
|
+
throw new PerpError("INVALID_PARAMS", `${p.venue} import needs one private key`);
|
|
745
|
+
const key = p.secret.key;
|
|
746
|
+
switch (p.venue) {
|
|
747
|
+
case "hyperliquid": return await importHyperliquid(p, agentName, key, replacing);
|
|
748
|
+
case "aster": return await importAster(p, agentName, key, replacing);
|
|
749
|
+
case "pacifica": return await importPacifica(p, agentName, key, replacing);
|
|
750
|
+
case "arcus": return await importArcus(p, agentName, key, replacing);
|
|
751
|
+
case "lighter":
|
|
752
|
+
case "lighter-rh":
|
|
753
|
+
return await importLighter(p, p.venue, agentName, key, replacing);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch (err) {
|
|
757
|
+
throw redacted(err, secrets, p.venue);
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
releaseLock(p.venue);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/** Human summary of an import (public data only). */
|
|
764
|
+
export function printImportOutcome(o) {
|
|
765
|
+
const def = getVenue(o.venue);
|
|
766
|
+
console.log(chalk.green.bold(`\n ${def.displayName} key imported (${o.network})\n`));
|
|
767
|
+
console.log(` Name: ${chalk.cyan(o.agentName)}`);
|
|
768
|
+
if (o.masterAddress)
|
|
769
|
+
console.log(` Master: ${chalk.cyan(o.masterAddress)} ${chalk.gray("(public address only — not on this computer)")}`);
|
|
770
|
+
if (o.agentAddress !== null)
|
|
771
|
+
console.log(` Agent address: ${chalk.cyan(o.agentAddress)}`);
|
|
772
|
+
if (o.publicKey !== null)
|
|
773
|
+
console.log(` Public key: ${chalk.cyan(o.publicKey)}`);
|
|
774
|
+
if (o.accountIndex !== null)
|
|
775
|
+
console.log(` Account index: ${chalk.cyan(String(o.accountIndex))}`);
|
|
776
|
+
if (o.apiKeyIndex !== null)
|
|
777
|
+
console.log(` API key slot: ${chalk.cyan(String(o.apiKeyIndex))}`);
|
|
778
|
+
if (o.accountId !== null)
|
|
779
|
+
console.log(` Account id: ${chalk.cyan(o.accountId)}`);
|
|
780
|
+
console.log(` Expires: ${o.expiresAt !== null ? chalk.cyan(o.expiresAt) : chalk.gray("unknown (set on the website)")}`);
|
|
781
|
+
console.log(` Stored in: ${chalk.gray(o.storage)}`);
|
|
782
|
+
if (o.verification.status === "verified")
|
|
783
|
+
console.log(` Verified: ${chalk.green("yes")} ${chalk.gray(`— ${o.verification.method}`)}`);
|
|
784
|
+
else
|
|
785
|
+
console.log(` Verified: ${chalk.yellow("NO")} ${chalk.yellow(`— ${o.verification.method}`)}`);
|
|
786
|
+
for (const w of o.warnings) {
|
|
787
|
+
// The withdraw-capable QFEX key warning is printed loudly (red); the rest as plain warnings.
|
|
788
|
+
if (o.venue === "qfex" && w.includes("\"Deposit and withdraw\" permission and is stored with an EMPTY passphrase"))
|
|
789
|
+
console.log(chalk.red.bold(`\n [WARNING] ${w}`));
|
|
790
|
+
else
|
|
791
|
+
console.log(chalk.yellow(`\n [warn] ${w}`));
|
|
792
|
+
}
|
|
793
|
+
console.log(chalk.gray(`\n ${IMPORT_SCOPE_NOTE}\n`));
|
|
794
|
+
}
|