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
package/dist/commands/init.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
import { networkOf, parseNetwork } from "../cli-flags.js";
|
|
1
2
|
import chalk from "chalk";
|
|
2
|
-
import { createInterface } from "readline";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { writeKeystoreAtomic } from "../agent-wallet/venue-keystore.js";
|
|
4
6
|
import { resolve } from "path";
|
|
5
|
-
import { loadSettings, saveSettings } from "../settings.js";
|
|
6
|
-
import { PerpError } from "../errors.js";
|
|
7
|
+
import { loadSettings, saveSettings, SHIPPED_DEFAULT_SLIPPAGE_BPS } from "../settings.js";
|
|
8
|
+
import { PerpError, classifyError } from "../errors.js";
|
|
7
9
|
import { jsonOk, jsonError, printJson } from "../utils.js";
|
|
8
10
|
import { resolvePassphrase } from "../agent-wallet/passphrase.js";
|
|
9
11
|
import { loadOws } from "../signer/ows-loader.js";
|
|
12
|
+
import { getVenue, listVenuesWithRole, networkConfig, tryResolveVenueId } from "../exchanges/venue.js";
|
|
13
|
+
import "../exchanges/venues/index.js";
|
|
14
|
+
import { maxDefaultSlippageBps, parseSlippageSettingBps } from "../exchanges/slippage.js";
|
|
15
|
+
import { agentApproveUnsupportedReason, approveVenueAgent, assertLighterSlot, isLighterFamilyVenue, deprecatedPassphraseOption, printApproveOutcome, promptHidden, verifyVenueAgent, warnDeprecatedPassphrase, } from "./agent.js";
|
|
16
|
+
import { importOwsWallet } from "./wallet.js";
|
|
17
|
+
import { agentNetworkOf, getAgent, listAgents } from "../agent-wallet/store.js";
|
|
18
|
+
import { IMPORT_KEY_WARNING, IMPORT_SCOPE_NOTE, UNVERIFIED_IMPORT_VENUES, assertDistinctImportSecrets, deleteImportedAgentLocal, importSecretFromLines, importSecretLabel, importSecretLineCount, importVenueAgent, printImportOutcome, } from "./agent-import.js";
|
|
10
19
|
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
11
20
|
const ENV_FILE = resolve(PERP_DIR, ".env");
|
|
12
21
|
// ── .env helpers ──────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// Legacy read path only. `perp setup` never writes secrets here (SSOT Rule #3:
|
|
24
|
+
// master keys live in the OWS vault, agent keys in OWS / ~/.perp/<venue>-agents/).
|
|
25
|
+
// `settings env` still uses these for existing legacy-key users (`wallet set` imports into OWS).
|
|
13
26
|
export { ENV_FILE, loadEnvFile, setEnvVar };
|
|
14
27
|
function ensurePerpDir() {
|
|
15
28
|
if (!existsSync(PERP_DIR))
|
|
@@ -35,14 +48,15 @@ function writeEnvFile(env) {
|
|
|
35
48
|
ensurePerpDir();
|
|
36
49
|
const lines = [
|
|
37
50
|
"# perp-cli configuration",
|
|
38
|
-
"#
|
|
51
|
+
"# Legacy key file — prefer the OWS vault (perp wallet import)",
|
|
39
52
|
"",
|
|
40
53
|
];
|
|
41
54
|
for (const [key, value] of Object.entries(env)) {
|
|
42
55
|
lines.push(`${key}=${value}`);
|
|
43
56
|
}
|
|
44
57
|
lines.push("");
|
|
45
|
-
|
|
58
|
+
// Atomic, explicit 0600 (writeFileSync's mode is ignored when the file already exists).
|
|
59
|
+
writeKeystoreAtomic(ENV_FILE, lines.join("\n"));
|
|
46
60
|
}
|
|
47
61
|
function setEnvVar(key, value) {
|
|
48
62
|
ensurePerpDir();
|
|
@@ -52,30 +66,13 @@ function setEnvVar(key, value) {
|
|
|
52
66
|
// Also set in current process
|
|
53
67
|
process.env[key] = value;
|
|
54
68
|
}
|
|
55
|
-
// ──
|
|
56
|
-
function ask(rl, question) {
|
|
57
|
-
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
58
|
-
}
|
|
59
|
-
function askChoice(rl, question, choices) {
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
const choiceStr = choices.map((c, i) => `${i + 1}) ${c}`).join(" ");
|
|
62
|
-
rl.question(`${question} [${choiceStr}]: `, (answer) => {
|
|
63
|
-
const trimmed = answer.trim();
|
|
64
|
-
const idx = parseInt(trimmed) - 1;
|
|
65
|
-
if (idx >= 0 && idx < choices.length)
|
|
66
|
-
return resolve(choices[idx]);
|
|
67
|
-
const match = choices.find((c) => c.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
68
|
-
resolve(match || choices[0]);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
// ── Exchange → env var mapping ────────────────────────────────
|
|
69
|
+
// ── Exchange → env var mapping (legacy venues only) ───────────
|
|
73
70
|
// v0.12.4 retired the legacy HMAC ASTER_API_KEY/SECRET path (Plan v3.0).
|
|
74
71
|
// v0.12.6 restores Aster as a normal EVM exchange: `ASTER_PRIVATE_KEY`
|
|
75
72
|
// (Tier 3 PK direct path) is collected like HL/LT, and the agent flow
|
|
76
73
|
// (`wallet agent approve aster`) is the recommended Tier 1 onboarding.
|
|
77
74
|
// Removing Aster entirely was over-correction — the entry must exist so
|
|
78
|
-
// `wallet import` / `wallet set`
|
|
75
|
+
// `wallet import` / `wallet set` treat all 4 legacy DEX uniformly.
|
|
79
76
|
export const EXCHANGE_ENV_MAP = {
|
|
80
77
|
pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
|
|
81
78
|
hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
|
|
@@ -117,201 +114,772 @@ export async function validateKey(chain, key) {
|
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
|
-
// ──
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
117
|
+
// ── Setup building blocks (shared by interactive + non-interactive) ──────────
|
|
118
|
+
/** Names (never values) of legacy private-key variables present in ~/.perp/.env. */
|
|
119
|
+
export function legacyEnvKeyNames() {
|
|
120
|
+
const env = loadEnvFile();
|
|
121
|
+
return Object.values(EXCHANGE_ENV_MAP).map(i => i.envKey).filter(k => env[k] !== undefined && env[k] !== "");
|
|
122
|
+
}
|
|
123
|
+
/** Trading venues from the registry (no hardcoded list). */
|
|
124
|
+
export function setupVenues() {
|
|
125
|
+
return listVenuesWithRole("trading");
|
|
126
|
+
}
|
|
127
|
+
/** Resolve one venue id/alias for setup: must be a trading venue with an environment on `network`. */
|
|
128
|
+
export function resolveSetupVenue(input, network) {
|
|
129
|
+
const id = tryResolveVenueId(input.trim());
|
|
130
|
+
if (id === null || !getVenue(id).roles.includes("trading")) {
|
|
131
|
+
throw new PerpError("INVALID_PARAMS", `Unknown trading venue "${input}"`, {
|
|
132
|
+
remediation: `Use one of: ${setupVenues().map(v => v.id).join(", ")}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
networkConfig(getVenue(id), network); // throws NOT_IMPLEMENTED when the venue has no such network
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
/** Parse a comma-separated venue list (ids or aliases), de-duplicated in input order. */
|
|
139
|
+
export function parseVenueList(csv, network) {
|
|
140
|
+
const parts = csv.split(/[,\s]+/).filter(p => p.length > 0);
|
|
141
|
+
if (parts.length === 0) {
|
|
142
|
+
throw new PerpError("INVALID_PARAMS", "No venues given", {
|
|
143
|
+
remediation: `Pass --venues <list>, e.g. --venues ${setupVenues().slice(0, 2).map(v => v.id).join(",")}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return [...new Set(parts.map(p => resolveSetupVenue(p, network)))];
|
|
147
|
+
}
|
|
148
|
+
/** Parse `<venue>=<bps>` per-venue slippage overrides; each bps must fit that venue's maxSlippageBps. */
|
|
149
|
+
export function parseVenueSlippage(entries, network) {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const eq = entry.indexOf("=");
|
|
153
|
+
if (eq <= 0) {
|
|
154
|
+
throw new PerpError("INVALID_PARAMS", `Invalid slippage override "${entry}"`, {
|
|
155
|
+
remediation: "Use <venue>=<bps>, e.g. --slippage arcus=100",
|
|
156
|
+
});
|
|
149
157
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
const venue = resolveSetupVenue(entry.slice(0, eq), network);
|
|
159
|
+
out[venue] = parseSlippageSettingBps(`slippageBps.${venue}`, entry.slice(eq + 1).trim(), getVenue(venue).maxSlippageBps);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** Write the wizard's choices to ~/.perp/settings.json (metadata only — never secrets). */
|
|
164
|
+
export function applySetupSettings(change) {
|
|
165
|
+
const settings = loadSettings();
|
|
166
|
+
if (change.owsActiveWallet !== null)
|
|
167
|
+
settings.owsActiveWallet = change.owsActiveWallet;
|
|
168
|
+
if (change.defaultExchange !== null)
|
|
169
|
+
settings.defaultExchange = change.defaultExchange;
|
|
170
|
+
if (change.defaultSlippageBps !== null)
|
|
171
|
+
settings.defaultSlippageBps = change.defaultSlippageBps;
|
|
172
|
+
for (const [venue, bps] of Object.entries(change.slippageBps)) {
|
|
173
|
+
settings.slippageBps[venue] = bps;
|
|
174
|
+
}
|
|
175
|
+
saveSettings(settings);
|
|
176
|
+
return settings;
|
|
177
|
+
}
|
|
178
|
+
/** Agent names already registered for `venue` on `network` in settings. */
|
|
179
|
+
function registeredAgentNames(venue, network) {
|
|
180
|
+
if (venue === "ondo" || venue === "arcus" || venue === "qfex") {
|
|
181
|
+
const map = loadSettings().agents?.[venue];
|
|
182
|
+
return map ? Object.values(map).filter(m => m.network === network).map(m => m.agentName) : [];
|
|
183
|
+
}
|
|
184
|
+
return listAgents(venue).filter(({ meta }) => agentNetworkOf(venue, meta) === network).map(({ meta }) => meta.agentName);
|
|
185
|
+
}
|
|
186
|
+
function rotateHint(venue) {
|
|
187
|
+
if (venue === "qfex")
|
|
188
|
+
return "perp wallet agent import qfex --replace";
|
|
189
|
+
return venue === "ondo" || venue === "arcus"
|
|
190
|
+
? `perp wallet agent approve ${venue} --rotate`
|
|
191
|
+
: `perp wallet agent rotate ${venue}`;
|
|
192
|
+
}
|
|
193
|
+
/** Why the wizard cannot register an agent for `venue` on `network`; null when it can (uniform wording, design §13.6). */
|
|
194
|
+
function approveBlocker(venue, network) {
|
|
195
|
+
const reason = agentApproveUnsupportedReason(venue, network, "approve");
|
|
196
|
+
return reason === null ? null : `${getVenue(venue).displayName} agent management on ${network} not supported — ${reason}`;
|
|
197
|
+
}
|
|
198
|
+
/** Real follow-up commands for the configured venues (all exist in this CLI). */
|
|
199
|
+
export function nextStepCommands(venues, network) {
|
|
200
|
+
const net = network === "testnet" ? " -n testnet" : "";
|
|
201
|
+
const cmds = [
|
|
202
|
+
`perp${net} wallet agent verify`,
|
|
203
|
+
`perp${net} portfolio`,
|
|
204
|
+
"perp funds info",
|
|
205
|
+
];
|
|
206
|
+
// `funds deposit` has subcommands for these venues only.
|
|
207
|
+
for (const v of venues) {
|
|
208
|
+
if (v === "pacifica" || v === "hyperliquid")
|
|
209
|
+
cmds.push(`perp${net} funds deposit ${v} <amount>`);
|
|
210
|
+
if (v === "lighter")
|
|
211
|
+
cmds.push(`perp${net} funds deposit lighter --help`);
|
|
212
|
+
}
|
|
213
|
+
for (const v of venues)
|
|
214
|
+
cmds.push(`perp${net} -e ${v} market list`);
|
|
215
|
+
cmds.push("perp settings show");
|
|
216
|
+
return cmds;
|
|
217
|
+
}
|
|
218
|
+
/** Plain-language slippage explanation (design §13.2, §13.10 #3). */
|
|
219
|
+
export function slippageExplanation(venues) {
|
|
220
|
+
const lines = [
|
|
221
|
+
"Market orders are sent as immediate-or-cancel (IOC) limit orders. The limit price is the",
|
|
222
|
+
"reference price moved by the slippage bound, so a market order never fills worse than that.",
|
|
223
|
+
`The shipped default is ${SHIPPED_DEFAULT_SLIPPAGE_BPS} bps (${SHIPPED_DEFAULT_SLIPPAGE_BPS / 100}%). 1 bps = 0.01%.`,
|
|
224
|
+
"The same bound applies to entries, closes, and TP/SL / stop triggers. Triggers use the trigger",
|
|
225
|
+
"price as the reference, so in a fast move a stop may NOT fill if price gaps beyond the bound.",
|
|
226
|
+
"A larger bound fills more reliably but can cost more; a smaller bound protects price but may",
|
|
227
|
+
"leave orders unfilled. Precedence: --slippage-bps flag → per-venue setting → global default.",
|
|
228
|
+
];
|
|
229
|
+
if (venues.length > 0) {
|
|
230
|
+
lines.push(`Per-venue maximum: ${venues.map(v => `${v} ${getVenue(v).maxSlippageBps}`).join(", ")} bps.`);
|
|
231
|
+
}
|
|
232
|
+
lines.push(`The global default must be ≤ ${maxDefaultSlippageBps()} bps (the tightest venue maximum).`);
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
function owsWalletInfo(name) {
|
|
236
|
+
const w = loadOws().listWallets().find(x => x.name === name);
|
|
237
|
+
if (!w)
|
|
238
|
+
return null;
|
|
239
|
+
const evm = w.accounts.find(a => a.chainId.startsWith("eip155:"));
|
|
240
|
+
const sol = w.accounts.find(a => a.chainId.startsWith("solana:"));
|
|
241
|
+
return { name: w.name, evm: evm ? evm.address : null, solana: sol ? sol.address : null };
|
|
242
|
+
}
|
|
243
|
+
/** Plain description of what approving `venue` signs (shown before confirmation). */
|
|
244
|
+
function signingSummary(p, master) {
|
|
245
|
+
const def = getVenue(p.venue);
|
|
246
|
+
const expiry = `valid for ${p.expiresIn}`;
|
|
247
|
+
switch (p.venue) {
|
|
248
|
+
case "hyperliquid":
|
|
249
|
+
return [`EIP-712 ApproveAgent signed by master EVM ${master.evm}: authorizes a new agent key to trade (${expiry}).`];
|
|
250
|
+
case "aster":
|
|
251
|
+
return [`EIP-712 ApproveAgent signed by master EVM ${master.evm}: perp=${p.canPerp ? "yes" : "no"}, spot=${p.canSpot ? "yes" : "no"}, withdraw=no (${expiry}).`];
|
|
252
|
+
case "pacifica":
|
|
253
|
+
return [`Ed25519 bind_agent_wallet message signed by master Solana ${master.solana}: binds a new agent key (${expiry}).`];
|
|
254
|
+
case "lighter":
|
|
255
|
+
case "lighter-rh":
|
|
256
|
+
return [`${def.displayName} ChangePubKey (L1 EIP-712 + L2) signed by master EVM ${master.evm}: registers a new API key in slot ${p.apiKeyIndex === null ? "<next free>" : p.apiKeyIndex}.`];
|
|
257
|
+
case "arcus":
|
|
258
|
+
return [`EIP-712 createApiKey signed by master EVM ${master.evm}: registers an Ed25519 API key for account index ${p.accountIndex} (${expiry}).`];
|
|
259
|
+
case "ondo":
|
|
260
|
+
return ["No signature. Your Ondo API key is stored encrypted in ~/.perp/ondo-agents/ and checked with one read-only account request."];
|
|
261
|
+
case "qfex":
|
|
262
|
+
return ["No signature. Your QFEX API key is stored encrypted in ~/.perp/qfex-agents/ and checked with read-only requests."];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function failed(venue, err) {
|
|
266
|
+
return { venue, status: "failed", error: classifyError(err, venue) };
|
|
267
|
+
}
|
|
268
|
+
function ttyPrompter() {
|
|
269
|
+
return {
|
|
270
|
+
async ask(question) {
|
|
271
|
+
// One readline per question so raw-mode hidden prompts never share stdin with it.
|
|
272
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
273
|
+
try {
|
|
274
|
+
return (await rl.question(question)).trim();
|
|
175
275
|
}
|
|
176
|
-
|
|
177
|
-
console.log(chalk.white(" Exchanges:"));
|
|
178
|
-
console.log(` 1) ${chalk.cyan("Pacifica")} ${chalk.gray("— Solana perps (USDC on Solana)")}`);
|
|
179
|
-
console.log(` 2) ${chalk.cyan("Hyperliquid")} ${chalk.gray("— EVM perps (USDC on Arbitrum)")}`);
|
|
180
|
-
console.log(` 3) ${chalk.cyan("Lighter")} ${chalk.gray("— EVM perps (USDC on Ethereum/Arb)")}`);
|
|
181
|
-
console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (use 'wallet agent approve aster')")}`);
|
|
182
|
-
console.log();
|
|
183
|
-
const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3,4 or 'all'): ");
|
|
184
|
-
const selected = parseExchangeChoice(exchangeChoice);
|
|
185
|
-
if (selected.length === 0) {
|
|
186
|
-
console.log(chalk.red("\n No exchanges selected.\n"));
|
|
276
|
+
finally {
|
|
187
277
|
rl.close();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
askHidden: (question) => promptHidden(question),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function askYesNo(p, question, defaultYes) {
|
|
284
|
+
for (;;) {
|
|
285
|
+
const a = (await p.ask(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
|
|
286
|
+
if (a === "")
|
|
287
|
+
return defaultYes;
|
|
288
|
+
if (a === "y" || a === "yes")
|
|
289
|
+
return true;
|
|
290
|
+
if (a === "n" || a === "no")
|
|
291
|
+
return false;
|
|
292
|
+
console.log(chalk.yellow(" Please answer y or n."));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function askNewPassphrase(p) {
|
|
296
|
+
for (;;) {
|
|
297
|
+
const pp = await p.askHidden(" Vault passphrase (hidden; empty = unencrypted): ");
|
|
298
|
+
const again = await p.askHidden(" Repeat passphrase: ");
|
|
299
|
+
if (pp === again)
|
|
300
|
+
return pp;
|
|
301
|
+
console.log(chalk.yellow(" Passphrases do not match — try again."));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Step 1 — pick / create / import the master OWS wallet, or skip it (name null: no master on this
|
|
306
|
+
* computer — every venue then uses an API key imported from the exchange website).
|
|
307
|
+
*/
|
|
308
|
+
async function stepMasterWallet(p) {
|
|
309
|
+
console.log(chalk.cyan.bold("\n Step 1/7 — Master wallet (OWS encrypted vault)\n"));
|
|
310
|
+
const ows = loadOws();
|
|
311
|
+
const active = loadSettings().owsActiveWallet;
|
|
312
|
+
// agent-<venue>-<master> wallets are delegated keys, never masters.
|
|
313
|
+
const wallets = ows.listWallets().filter(w => !w.name.startsWith("agent-"));
|
|
314
|
+
wallets.forEach((w, i) => {
|
|
315
|
+
const evm = w.accounts.find(a => a.chainId.startsWith("eip155:"));
|
|
316
|
+
const sol = w.accounts.find(a => a.chainId.startsWith("solana:"));
|
|
317
|
+
const mark = w.name === active ? chalk.cyan(" (active)") : "";
|
|
318
|
+
console.log(` ${i + 1}) ${chalk.white.bold(w.name)}${mark} ${chalk.gray(`EVM ${evm ? evm.address : "-"} SOL ${sol ? sol.address : "-"}`)}`);
|
|
319
|
+
});
|
|
320
|
+
if (wallets.length === 0)
|
|
321
|
+
console.log(chalk.gray(" (no wallets in the vault yet)"));
|
|
322
|
+
console.log(` c) create a new wallet`);
|
|
323
|
+
console.log(` i) import an existing key / mnemonic`);
|
|
324
|
+
console.log(` s) skip — no master wallet on this computer (use API keys created on the exchange websites)`);
|
|
325
|
+
for (;;) {
|
|
326
|
+
const a = (await p.ask("\n Choose: ")).toLowerCase();
|
|
327
|
+
const idx = Number(a);
|
|
328
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= wallets.length)
|
|
329
|
+
return { name: wallets[idx - 1].name, passphrase: null };
|
|
330
|
+
if (a === "s") {
|
|
331
|
+
console.log(chalk.gray(` No master wallet: you will import API keys created on each exchange website. ${IMPORT_SCOPE_NOTE}`));
|
|
332
|
+
return { name: null, passphrase: null };
|
|
333
|
+
}
|
|
334
|
+
if (a === "c" || a === "i") {
|
|
335
|
+
const name = await p.ask(" New wallet name: ");
|
|
336
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(name) || name.startsWith("agent-")) {
|
|
337
|
+
console.log(chalk.yellow(" Use 1-64 letters, digits, '.', '_' or '-' (not starting with 'agent-')."));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (ows.listWallets().some(w => w.name === name)) {
|
|
341
|
+
console.log(chalk.yellow(` A wallet named "${name}" already exists.`));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (a === "c") {
|
|
345
|
+
const pp = await askNewPassphrase(p);
|
|
346
|
+
const w = ows.createWallet(name, pp);
|
|
347
|
+
console.log(chalk.green(` Created ${w.name}.`) + chalk.gray(" Back it up with: perp wallet backup"));
|
|
348
|
+
return { name, passphrase: pp };
|
|
349
|
+
}
|
|
350
|
+
const kind = (await p.ask(" Import type — 1) EVM key 2) Solana key 3) EVM + Solana keys 4) mnemonic: ")).trim();
|
|
351
|
+
let input;
|
|
352
|
+
if (kind === "1")
|
|
353
|
+
input = { kind: "single", chain: "evm", key: await p.askHidden(" EVM private key (hidden): ") };
|
|
354
|
+
else if (kind === "2")
|
|
355
|
+
input = { kind: "single", chain: "solana", key: await p.askHidden(" Solana private key (hidden): ") };
|
|
356
|
+
else if (kind === "3")
|
|
357
|
+
input = { kind: "dual", evm: await p.askHidden(" EVM private key (hidden): "), solana: await p.askHidden(" Solana private key (hidden): ") };
|
|
358
|
+
else if (kind === "4")
|
|
359
|
+
input = { kind: "mnemonic", mnemonic: await p.askHidden(" Mnemonic (hidden): ") };
|
|
360
|
+
else {
|
|
361
|
+
console.log(chalk.yellow(" Choose 1-4."));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const pp = await askNewPassphrase(p);
|
|
365
|
+
try {
|
|
366
|
+
const w = await importOwsWallet(name, pp, input);
|
|
367
|
+
console.log(chalk.green(` Imported ${w.name} into the OWS vault.`));
|
|
368
|
+
return { name, passphrase: pp };
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
// importOwsWallet throws a PerpError whose message is already redacted (redactSecrets);
|
|
372
|
+
// anything else gets a fixed message — key material must never be echoed.
|
|
373
|
+
console.log(chalk.red(err instanceof PerpError ? ` ${err.message}` : " Import failed: the key / mnemonic was rejected (input not shown)."));
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk.yellow(` Enter ${wallets.length > 0 ? `1-${wallets.length}, ` : ""}c, i, or s.`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** `preselected` is the global -n network: the blank answer keeps it. */
|
|
381
|
+
async function stepNetwork(p, preselected) {
|
|
382
|
+
console.log(chalk.cyan.bold("\n Step 2/7 — Network\n"));
|
|
383
|
+
const withTestnet = setupVenues().filter(v => v.networks.testnet !== null).map(v => v.id);
|
|
384
|
+
console.log(chalk.gray(` testnet is available for: ${withTestnet.join(", ")}`));
|
|
385
|
+
const def = preselected === "mainnet" ? "1" : "2";
|
|
386
|
+
for (;;) {
|
|
387
|
+
const a = (await p.ask(` 1) mainnet 2) testnet [${def}]: `)).toLowerCase();
|
|
388
|
+
if (a === "")
|
|
389
|
+
return preselected;
|
|
390
|
+
if (a === "1" || a === "mainnet")
|
|
391
|
+
return "mainnet";
|
|
392
|
+
if (a === "2" || a === "testnet")
|
|
393
|
+
return "testnet";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function stepVenues(p, network) {
|
|
397
|
+
console.log(chalk.cyan.bold("\n Step 3/7 — Venues\n"));
|
|
398
|
+
const all = setupVenues();
|
|
399
|
+
const offered = all.filter(v => v.networks[network] !== null);
|
|
400
|
+
offered.forEach((v, i) => {
|
|
401
|
+
const net = networkConfig(v, network);
|
|
402
|
+
console.log(` ${i + 1}) ${chalk.cyan(v.displayName.padEnd(12))} ${chalk.gray(`${v.id} — ${net.collateral[0]}${net.collateral.length > 1 ? " +" + (net.collateral.length - 1) : ""} collateral on ${net.settlementChain}`)}`);
|
|
403
|
+
if (v.restrictedRegions.length > 0) {
|
|
404
|
+
console.log(chalk.yellow(` ⚠ not available to residents of: ${v.restrictedRegions.join(", ")}`));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const missing = all.filter(v => v.networks[network] === null);
|
|
408
|
+
if (missing.length > 0)
|
|
409
|
+
console.log(chalk.gray(` (no ${network}: ${missing.map(v => v.id).join(", ")})`));
|
|
410
|
+
for (;;) {
|
|
411
|
+
const a = (await p.ask("\n Venues (e.g. 1,3 or 'all'): ")).toLowerCase();
|
|
412
|
+
if (a === "all")
|
|
413
|
+
return offered.map(v => v.id);
|
|
414
|
+
const parts = a.split(/[,\s]+/).filter(x => x.length > 0);
|
|
415
|
+
const picked = [];
|
|
416
|
+
let bad = null;
|
|
417
|
+
for (const part of parts) {
|
|
418
|
+
const n = Number(part);
|
|
419
|
+
if (Number.isInteger(n) && n >= 1 && n <= offered.length)
|
|
420
|
+
picked.push(offered[n - 1].id);
|
|
421
|
+
else {
|
|
422
|
+
const id = tryResolveVenueId(part);
|
|
423
|
+
if (id !== null && offered.some(v => v.id === id))
|
|
424
|
+
picked.push(id);
|
|
425
|
+
else
|
|
426
|
+
bad = part;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (bad !== null)
|
|
430
|
+
console.log(chalk.yellow(` Unknown choice "${bad}".`));
|
|
431
|
+
else if (picked.length === 0)
|
|
432
|
+
console.log(chalk.yellow(" Pick at least one venue."));
|
|
433
|
+
else
|
|
434
|
+
return [...new Set(picked)];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function venueApproveParams(p, venue, network, master) {
|
|
438
|
+
const base = {
|
|
439
|
+
venue, network, master, agentName: null, expiresIn: "90d",
|
|
440
|
+
canPerp: true, canSpot: false, canWithdraw: false, rotate: false, passphrase: null,
|
|
441
|
+
apiKeyIndex: null, forceSlot: false, accountIndex: 0, aster: {}, ondoKey: null,
|
|
442
|
+
};
|
|
443
|
+
if (venue === "aster") {
|
|
444
|
+
base.canSpot = await askYesNo(p, " Also allow the agent to trade Aster spot?", false);
|
|
445
|
+
}
|
|
446
|
+
else if (isLighterFamilyVenue(venue)) {
|
|
447
|
+
const allowed = venue === "lighter-rh" ? "4-254 except 157" : "4-254";
|
|
448
|
+
console.log(chalk.gray(` API key slots 0-3${venue === "lighter-rh" ? " and 157" : ""} are reserved by the venue's frontend.`));
|
|
449
|
+
for (;;) {
|
|
450
|
+
const a = await p.ask(` API key slot (${allowed}, blank = next free): `);
|
|
451
|
+
if (a === "")
|
|
452
|
+
break;
|
|
453
|
+
try {
|
|
454
|
+
assertLighterSlot(venue, Number(a));
|
|
455
|
+
base.apiKeyIndex = Number(a);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.log(chalk.yellow(` ${err instanceof Error ? err.message : String(err)}`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (venue === "arcus") {
|
|
464
|
+
console.log(chalk.yellow(" Arcus API access is whitelist-gated: your master EVM address must be on the Arcus access"));
|
|
465
|
+
console.log(chalk.yellow(" whitelist (request at https://arcus.xyz), otherwise registration is rejected."));
|
|
466
|
+
for (;;) {
|
|
467
|
+
const a = await p.ask(" Arcus account index (0-9, or 255 for all; trading needs 0-9) [0]: ");
|
|
468
|
+
const n = a === "" ? 0 : Number(a);
|
|
469
|
+
if (Number.isInteger(n) && ((n >= 0 && n <= 9) || n === 255)) {
|
|
470
|
+
base.accountIndex = n;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
console.log(chalk.yellow(" Enter 0-9 or 255."));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (venue === "ondo") {
|
|
477
|
+
console.log(chalk.gray(" Create an API key in the Ondo Perps web app first. The secret is read hidden and stored encrypted."));
|
|
478
|
+
const keyId = await p.ask(" Ondo API key id (blank = skip): ");
|
|
479
|
+
if (keyId === "")
|
|
480
|
+
return null;
|
|
481
|
+
const secret = await p.askHidden(" Ondo API secret (hidden): ");
|
|
482
|
+
base.ondoKey = { keyId, secret };
|
|
483
|
+
}
|
|
484
|
+
return base;
|
|
485
|
+
}
|
|
486
|
+
/** Prompt for the public inputs + the hidden key of a website-created key (`wallet agent import`). */
|
|
487
|
+
async function venueImportParams(p, venue, network) {
|
|
488
|
+
const def = getVenue(venue);
|
|
489
|
+
console.log(chalk.gray(` Create the key on the ${def.displayName} ${network} website first (API / agent wallet page).`));
|
|
490
|
+
const params = {
|
|
491
|
+
venue, network, agentName: null, masterAddress: null, accountIndex: null, apiKeyIndex: null, expiresIn: null, replace: false,
|
|
492
|
+
accountId: null,
|
|
493
|
+
// QFEX: the resolved account UUID is shown and confirmed before the key is stored.
|
|
494
|
+
confirmAccount: venue !== "qfex" ? null : async (binding) => {
|
|
495
|
+
console.log(chalk.white(` This key will trade QFEX account ${chalk.cyan(binding.accountId)} (${binding.isPrimaryAccount ? "primary" : "subaccount"}).`));
|
|
496
|
+
return askYesNo(p, " Store the key for this account?", false);
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
const askInt = async (q, optional) => {
|
|
500
|
+
for (;;) {
|
|
501
|
+
const a = await p.ask(q);
|
|
502
|
+
if (a === "" && optional)
|
|
503
|
+
return null;
|
|
504
|
+
const n = Number(a);
|
|
505
|
+
if (a !== "" && Number.isInteger(n) && n >= 0)
|
|
506
|
+
return n;
|
|
507
|
+
console.log(chalk.yellow(" Enter a non-negative integer."));
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
if (venue === "hyperliquid") {
|
|
511
|
+
const a = await p.ask(" Master address (blank = read it from Hyperliquid): ");
|
|
512
|
+
params.masterAddress = a === "" ? null : a;
|
|
513
|
+
}
|
|
514
|
+
else if (venue === "aster" || venue === "pacifica" || venue === "arcus") {
|
|
515
|
+
for (;;) {
|
|
516
|
+
const a = await p.ask(` Master address you log in with on ${def.displayName} (${venue === "pacifica" ? "Solana" : "EVM"}; blank = skip venue): `);
|
|
517
|
+
if (a === "")
|
|
518
|
+
return null;
|
|
519
|
+
params.masterAddress = a;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
if (venue === "arcus")
|
|
523
|
+
params.accountIndex = await askInt(" Account index the key is scoped to (blank = as registered): ", true);
|
|
524
|
+
}
|
|
525
|
+
else if (isLighterFamilyVenue(venue)) {
|
|
526
|
+
params.accountIndex = await askInt(" Account index (shown with the API key): ", false);
|
|
527
|
+
params.apiKeyIndex = await askInt(" API key index / slot of the key: ", false);
|
|
528
|
+
}
|
|
529
|
+
let secret;
|
|
530
|
+
if (venue === "ondo") {
|
|
531
|
+
const keyId = await p.ask(" Ondo API key id (ondoKeyId_…; blank = skip): ");
|
|
532
|
+
if (keyId === "")
|
|
533
|
+
return null;
|
|
534
|
+
secret = { kind: "ondo", keyId, secret: await p.askHidden(" Ondo API secret (hidden): ") };
|
|
535
|
+
}
|
|
536
|
+
else if (venue === "qfex") {
|
|
537
|
+
console.log(chalk.gray(" No signature. Your QFEX API key is stored encrypted in ~/.perp/qfex-agents/ and checked with read-only requests."));
|
|
538
|
+
console.log(chalk.gray(" Create it in QFEX Developer Settings (2FA). For trading, leave \"Deposit and withdraw\" OFF."));
|
|
539
|
+
const publicKey = await p.ask(" QFEX public key (blank = skip): ");
|
|
540
|
+
if (publicKey === "")
|
|
541
|
+
return null;
|
|
542
|
+
secret = { kind: "qfex", publicKey, secret: await p.askHidden(" QFEX secret (hidden): ") };
|
|
543
|
+
const sub = await p.ask(" Subaccount id (blank = primary): ");
|
|
544
|
+
params.accountId = sub === "" ? null : sub;
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
const key = await p.askHidden(` ${importSecretLabel(venue)} (hidden; blank = skip): `);
|
|
548
|
+
if (key === "")
|
|
549
|
+
return null;
|
|
550
|
+
secret = { kind: "key", key };
|
|
551
|
+
}
|
|
552
|
+
return { ...params, secret };
|
|
553
|
+
}
|
|
554
|
+
async function stepAgents(p, venues, network, master, getPassphrase) {
|
|
555
|
+
console.log(chalk.cyan.bold("\n Step 4/7 — Agent wallets / API keys\n"));
|
|
556
|
+
console.log(chalk.gray(" An agent is a separate trading key. perp-cli signs orders with it, so the master key is not"));
|
|
557
|
+
console.log(chalk.gray(" needed for every trade. Two ways to get one per venue:"));
|
|
558
|
+
console.log(chalk.gray(" approve — this computer's master wallet authorizes a new agent (one signature)"));
|
|
559
|
+
console.log(chalk.gray(" import — paste an agent / API key you created on the exchange website (no master needed here)"));
|
|
560
|
+
console.log(chalk.gray(` ${IMPORT_SCOPE_NOTE}`));
|
|
561
|
+
console.log(chalk.gray(" Neither moves funds. Withdrawal permission is never granted here."));
|
|
562
|
+
const info = master !== null ? owsWalletInfo(master) : null;
|
|
563
|
+
if (master !== null && !info)
|
|
564
|
+
throw new PerpError("KEY_NOT_FOUND", `OWS wallet "${master}" not found`, { remediation: "perp wallet list" });
|
|
565
|
+
const results = [];
|
|
566
|
+
for (const venue of venues) {
|
|
567
|
+
const def = getVenue(venue);
|
|
568
|
+
console.log(chalk.white.bold(`\n ${def.displayName}`));
|
|
569
|
+
const existing = registeredAgentNames(venue, network);
|
|
570
|
+
if (existing.length > 0) {
|
|
571
|
+
const reason = `already registered (${existing.join(", ")}); to replace: ${rotateHint(venue)}`;
|
|
572
|
+
console.log(chalk.gray(` Skipped: ${reason}.`));
|
|
573
|
+
results.push({ venue, status: "skipped", reason });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
// Why "approve" is not offered for this venue (null = it is). Import is always offered.
|
|
577
|
+
let approveBlocked = venue === "ondo" ? "Ondo keys are always created on the website"
|
|
578
|
+
: venue === "qfex" ? "QFEX keys are always created on the website (2FA)"
|
|
579
|
+
: approveBlocker(venue, network);
|
|
580
|
+
if (approveBlocked === null && master === null)
|
|
581
|
+
approveBlocked = "no master wallet on this computer";
|
|
582
|
+
if (approveBlocked === null && info !== null && ((def.masterKeyKind === "solana" && info.solana === null) || (def.masterKeyKind === "evm" && info.evm === null))) {
|
|
583
|
+
approveBlocked = `wallet "${master}" has no ${def.masterKeyKind} account`;
|
|
584
|
+
}
|
|
585
|
+
let method;
|
|
586
|
+
if (approveBlocked === null) {
|
|
587
|
+
for (;;) {
|
|
588
|
+
const a = (await p.ask(" 1) approve with this computer's master wallet 2) import a key created on the website 3) skip [1]: ")).toLowerCase();
|
|
589
|
+
if (a === "" || a === "1" || a === "y" || a === "yes") {
|
|
590
|
+
method = "approve";
|
|
591
|
+
break;
|
|
215
592
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const key = await ask(rl, ` ${info.label} private key (0x...): `);
|
|
220
|
-
if (!key) {
|
|
221
|
-
console.log(chalk.gray(" Skipped."));
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const { valid, address } = await validateKey("evm", key);
|
|
225
|
-
if (!valid) {
|
|
226
|
-
console.log(chalk.red(" Invalid key, skipped."));
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const normalized = key.startsWith("0x") ? key : `0x${key}`;
|
|
230
|
-
newEnv[info.envKey] = normalized;
|
|
231
|
-
console.log(` ${chalk.green("OK")} ${chalk.gray(address)}`);
|
|
232
|
-
}
|
|
593
|
+
if (a === "2" || a === "import") {
|
|
594
|
+
method = "import";
|
|
595
|
+
break;
|
|
233
596
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const key = await ask(rl, " Solana private key (base58): ");
|
|
238
|
-
if (key) {
|
|
239
|
-
const { valid, address } = await validateKey("solana", key);
|
|
240
|
-
if (valid) {
|
|
241
|
-
newEnv.PACIFICA_PRIVATE_KEY = key;
|
|
242
|
-
console.log(` ${chalk.green("OK")} ${chalk.gray(address)}`);
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
console.log(chalk.red(" Invalid Solana key, skipped."));
|
|
246
|
-
}
|
|
597
|
+
if (a === "3" || a === "n" || a === "no") {
|
|
598
|
+
method = "skip";
|
|
599
|
+
break;
|
|
247
600
|
}
|
|
601
|
+
console.log(chalk.yellow(" Enter 1, 2 or 3."));
|
|
248
602
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// Summary
|
|
265
|
-
console.log(chalk.cyan.bold("\n Setup Complete!\n"));
|
|
266
|
-
console.log(` Config: ${chalk.gray("~/.perp/.env")}`);
|
|
267
|
-
console.log(` Default: ${chalk.cyan(settings.defaultExchange || selected[0])}`);
|
|
268
|
-
for (const ex of selected) {
|
|
269
|
-
const info = EXCHANGE_ENV_MAP[ex];
|
|
270
|
-
const key = newEnv[info.envKey];
|
|
271
|
-
if (key) {
|
|
272
|
-
console.log(` ${chalk.cyan(ex.padEnd(14))} ${chalk.green("configured")}`);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
console.log(chalk.gray(` Approve here is not available (${approveBlocked}).`));
|
|
606
|
+
method = (await askYesNo(p, ` Import a key created on the ${def.displayName} website?`, true)) ? "import" : "skip";
|
|
607
|
+
}
|
|
608
|
+
if (method === "skip") {
|
|
609
|
+
results.push({ venue, status: "skipped", reason: approveBlocked ?? "skipped by user" });
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
if (method === "import") {
|
|
614
|
+
const params = await venueImportParams(p, venue, network);
|
|
615
|
+
if (params === null) {
|
|
616
|
+
results.push({ venue, status: "skipped", reason: "skipped by user" });
|
|
617
|
+
continue;
|
|
273
618
|
}
|
|
619
|
+
const outcome = await importVenueAgent(params);
|
|
620
|
+
printImportOutcome(outcome);
|
|
621
|
+
results.push({ venue, status: "imported", agentName: outcome.agentName, verification: outcome.verification });
|
|
622
|
+
continue;
|
|
274
623
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
624
|
+
const params = await venueApproveParams(p, venue, network, master);
|
|
625
|
+
if (params === null) {
|
|
626
|
+
results.push({ venue, status: "skipped", reason: "skipped by user" });
|
|
627
|
+
continue;
|
|
279
628
|
}
|
|
280
|
-
|
|
281
|
-
|
|
629
|
+
console.log(chalk.white(`\n ${network === "mainnet" ? chalk.red.bold("MAINNET") : "testnet"} — this will:`));
|
|
630
|
+
for (const line of signingSummary(params, info))
|
|
631
|
+
console.log(` ${line}`);
|
|
632
|
+
console.log(chalk.gray(" No deposit, withdrawal, or trade is made."));
|
|
633
|
+
if (!(await askYesNo(p, " Sign and submit?", false))) {
|
|
634
|
+
results.push({ venue, status: "skipped", reason: "not confirmed" });
|
|
635
|
+
continue;
|
|
282
636
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
637
|
+
params.passphrase = await getPassphrase();
|
|
638
|
+
const outcome = await approveVenueAgent(params);
|
|
639
|
+
printApproveOutcome(outcome);
|
|
640
|
+
results.push({ venue, status: "approved", agentName: "meta" in outcome ? outcome.meta.agentName : outcome.agentName });
|
|
286
641
|
}
|
|
287
642
|
catch (err) {
|
|
288
|
-
|
|
289
|
-
|
|
643
|
+
const r = failed(venue, err);
|
|
644
|
+
if (r.status === "failed") {
|
|
645
|
+
console.log(chalk.red(` ✗ ${def.displayName}: [${r.error.code}] ${r.error.message}`));
|
|
646
|
+
if (r.error.remediation)
|
|
647
|
+
console.log(chalk.gray(` ${r.error.remediation}`));
|
|
648
|
+
}
|
|
649
|
+
results.push(r);
|
|
290
650
|
}
|
|
651
|
+
}
|
|
652
|
+
return results;
|
|
653
|
+
}
|
|
654
|
+
async function stepSlippage(p, venues, network) {
|
|
655
|
+
console.log(chalk.cyan.bold("\n Step 5/7 — Slippage bound\n"));
|
|
656
|
+
for (const line of slippageExplanation(venues))
|
|
657
|
+
console.log(chalk.gray(` ${line}`));
|
|
658
|
+
const s = loadSettings();
|
|
659
|
+
const shipped = s.defaultSlippageBps === SHIPPED_DEFAULT_SLIPPAGE_BPS ? " (shipped default)" : "";
|
|
660
|
+
console.log(`\n Current default: ${chalk.cyan(String(s.defaultSlippageBps))} bps${chalk.gray(shipped)}`);
|
|
661
|
+
for (const [v, bps] of Object.entries(s.slippageBps))
|
|
662
|
+
console.log(` Current ${v}: ${chalk.cyan(String(bps))} bps`);
|
|
663
|
+
let defaultSlippageBps = null;
|
|
664
|
+
const max = maxDefaultSlippageBps();
|
|
665
|
+
for (;;) {
|
|
666
|
+
const a = await p.ask(`\n Global default in bps (blank = keep ${s.defaultSlippageBps}): `);
|
|
667
|
+
if (a === "")
|
|
668
|
+
break;
|
|
669
|
+
try {
|
|
670
|
+
defaultSlippageBps = parseSlippageSettingBps("defaultSlippageBps", a, max);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
console.log(chalk.yellow(` ${err instanceof Error ? err.message : String(err)}`));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const slippageBps = {};
|
|
678
|
+
for (;;) {
|
|
679
|
+
const a = await p.ask(" Per-venue overrides as venue=bps, comma-separated (blank = none): ");
|
|
680
|
+
if (a === "")
|
|
681
|
+
break;
|
|
682
|
+
try {
|
|
683
|
+
Object.assign(slippageBps, parseVenueSlippage(a.split(/[,\s]+/).filter(x => x.length > 0), network));
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
console.log(chalk.yellow(` ${err instanceof PerpError ? err.structured.message : String(err)}`));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return { defaultSlippageBps, slippageBps };
|
|
691
|
+
}
|
|
692
|
+
async function stepDefaultVenue(p, venues) {
|
|
693
|
+
console.log(chalk.cyan.bold("\n Step 6/7 — Default venue\n"));
|
|
694
|
+
if (venues.length === 1) {
|
|
695
|
+
console.log(` ${chalk.cyan(venues[0])} (only selected venue)`);
|
|
696
|
+
return venues[0];
|
|
697
|
+
}
|
|
698
|
+
venues.forEach((v, i) => console.log(` ${i + 1}) ${v}`));
|
|
699
|
+
for (;;) {
|
|
700
|
+
const n = Number(await p.ask(" Default venue for commands without -e: "));
|
|
701
|
+
if (Number.isInteger(n) && n >= 1 && n <= venues.length)
|
|
702
|
+
return venues[n - 1];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function stepSummary(venues, network, master, results, getPassphrase) {
|
|
706
|
+
console.log(chalk.cyan.bold("\n Step 7/7 — Summary\n"));
|
|
707
|
+
const s = loadSettings();
|
|
708
|
+
console.log(` Wallet: ${master !== null ? chalk.white.bold(s.owsActiveWallet) : chalk.gray("none on this computer (imported API keys only)")}`);
|
|
709
|
+
console.log(` Network: ${network}`);
|
|
710
|
+
console.log(` Default: ${chalk.cyan(s.defaultExchange)}`);
|
|
711
|
+
console.log(` Slippage: ${s.defaultSlippageBps} bps default${Object.entries(s.slippageBps).map(([v, b]) => `, ${v} ${b}`).join("")}`);
|
|
712
|
+
for (const r of results) {
|
|
713
|
+
const text = r.status === "approved" ? chalk.green(`agent ${r.agentName} registered`)
|
|
714
|
+
: r.status === "imported" ? (r.verification.status === "verified"
|
|
715
|
+
? chalk.green(`key ${r.agentName} imported (verified)`)
|
|
716
|
+
: chalk.yellow(`key ${r.agentName} imported — NOT verified: ${r.verification.method}`))
|
|
717
|
+
: r.status === "skipped" ? chalk.gray(`skipped — ${r.reason}`)
|
|
718
|
+
: chalk.red(`failed — [${r.error.code}] ${r.error.message}`);
|
|
719
|
+
console.log(` ${r.venue.padEnd(12)} ${text}`);
|
|
720
|
+
}
|
|
721
|
+
// Read-only live check for every venue whose agent was approved here (imported keys were
|
|
722
|
+
// checked at import time — the result is shown above).
|
|
723
|
+
const approvedHere = new Set(results.filter(r => r.status === "approved").map(r => r.venue));
|
|
724
|
+
const toVerify = master === null ? [] : venues.filter(v => approvedHere.has(v) && registeredAgentNames(v, network).length > 0 && approveBlocker(v, network) === null);
|
|
725
|
+
if (toVerify.length > 0) {
|
|
726
|
+
console.log(chalk.white.bold("\n Agent verification (read-only):"));
|
|
727
|
+
for (const v of toVerify) {
|
|
728
|
+
try {
|
|
729
|
+
const r = await verifyVenueAgent(v, network, { master, passphrase: await getPassphrase() });
|
|
730
|
+
const icon = r.registered ? chalk.green("✓") : chalk.yellow("○");
|
|
731
|
+
console.log(` ${icon} ${v}: ${r.count} key(s) on the venue`);
|
|
732
|
+
for (const w of r.warnings ?? [])
|
|
733
|
+
console.log(chalk.yellow(` [warn] ${w}`));
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
const e = classifyError(err, v);
|
|
737
|
+
console.log(chalk.gray(` – ${v}: [${e.code}] ${e.message}`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (results.some(r => r.status === "imported"))
|
|
742
|
+
console.log(chalk.gray(`\n ${IMPORT_SCOPE_NOTE}`));
|
|
743
|
+
console.log(chalk.white.bold("\n Next steps:"));
|
|
744
|
+
for (const c of nextStepCommands(venues, network))
|
|
745
|
+
console.log(` ${chalk.green(c)}`);
|
|
746
|
+
console.log();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Optional builder-fee step — offered only when settings `builder.<venue>` already holds a
|
|
750
|
+
* value for a chosen venue (builder fees ship off). Explains the fee and approves only after
|
|
751
|
+
* an explicit "y" (default No). Never configures a builder itself.
|
|
752
|
+
*/
|
|
753
|
+
export async function stepBuilder(p, venues, network, master, getPassphrase) {
|
|
754
|
+
const { builderTargetOf } = await import("../exchanges/builder-fee.js");
|
|
755
|
+
const builder = loadSettings().builder;
|
|
756
|
+
const configured = venues.filter(v => builderTargetOf(builder, v) !== null);
|
|
757
|
+
if (configured.length === 0)
|
|
758
|
+
return;
|
|
759
|
+
const { builderApprovalPlan, approveBuilderOnVenue } = await import("./builder.js");
|
|
760
|
+
console.log(chalk.cyan.bold("\n Optional — Builder fees\n"));
|
|
761
|
+
console.log(chalk.gray(" A builder fee is an extra fee on YOUR perp orders, paid to the builder in settings builder.<venue>."));
|
|
762
|
+
console.log(chalk.gray(" It is attached only after the venue approval is confirmed. Default: skip.\n"));
|
|
763
|
+
for (const v of configured) {
|
|
764
|
+
try {
|
|
765
|
+
const plan = builderApprovalPlan(v, network, master);
|
|
766
|
+
const fee = plan.feeKind === "share" ? "fee share (you pay the normal fee)" : `${plan.feeKind === "max" ? "up to " : ""}${plan.feeBps} bps`;
|
|
767
|
+
if (plan.approval === "none") {
|
|
768
|
+
console.log(` ${v.padEnd(12)} ${plan.identity} ${fee} — no approval step; attached as configured (remove: perp settings unset builder.${v})`);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (plan.master === null) {
|
|
772
|
+
console.log(` ${v.padEnd(12)} ${chalk.yellow(`no master wallet here — approve builder ${plan.identity} (${fee}) on the ${getVenue(v).displayName} website`)}`);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (!(await askYesNo(p, ` Approve builder ${plan.identity} to charge ${fee} on your ${v} perp orders (cap ${plan.capBps} bps)?`, false))) {
|
|
776
|
+
console.log(chalk.gray(` ${v}: skipped — no fee is attached until approved (perp wallet builder approve ${v})`));
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
await approveBuilderOnVenue(plan, await getPassphrase());
|
|
780
|
+
console.log(chalk.green(` ${v}: builder ${plan.identity} approved (${fee})`));
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
const e = classifyError(err, v);
|
|
784
|
+
console.log(chalk.yellow(` ${v}: builder approval not done — [${e.code}] ${e.message}`));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/** Interactive `perp setup`. All prompts go through `p`; nothing secret is printed or written to .env. */
|
|
789
|
+
export async function runInteractiveSetup(p, passphraseFlag, preselectedNetwork) {
|
|
790
|
+
console.log(chalk.cyan.bold("\n perp-cli Setup Wizard\n"));
|
|
791
|
+
const legacy = legacyEnvKeyNames();
|
|
792
|
+
if (legacy.length > 0) {
|
|
793
|
+
console.log(chalk.yellow(` ⚠ ${ENV_FILE} holds raw private keys (${legacy.join(", ")}).`));
|
|
794
|
+
console.log(chalk.yellow(" They keep working, but consider moving them into the encrypted OWS vault:"));
|
|
795
|
+
console.log(chalk.yellow(" perp wallet import --name <name> [--chain evm|solana|dual] (hidden prompt / stdin), then"));
|
|
796
|
+
console.log(chalk.yellow(" perp settings env remove <NAME> for each entry. This wizard does not touch them.\n"));
|
|
797
|
+
}
|
|
798
|
+
const master = await stepMasterWallet(p);
|
|
799
|
+
let passphrase = master.passphrase;
|
|
800
|
+
const getPassphrase = async () => {
|
|
801
|
+
if (passphrase !== null)
|
|
802
|
+
return passphrase;
|
|
803
|
+
if (master.name === null)
|
|
804
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "No master wallet on this computer");
|
|
805
|
+
const resolved = await resolvePassphrase({ flag: passphraseFlag });
|
|
806
|
+
passphrase = resolved !== null ? resolved : await p.askHidden(` Passphrase for wallet "${master.name}" (hidden): `);
|
|
807
|
+
return passphrase;
|
|
808
|
+
};
|
|
809
|
+
const network = await stepNetwork(p, preselectedNetwork);
|
|
810
|
+
const venues = await stepVenues(p, network);
|
|
811
|
+
const results = await stepAgents(p, venues, network, master.name, getPassphrase);
|
|
812
|
+
const slip = await stepSlippage(p, venues, network);
|
|
813
|
+
const defaultVenue = await stepDefaultVenue(p, venues);
|
|
814
|
+
applySetupSettings({
|
|
815
|
+
owsActiveWallet: master.name,
|
|
816
|
+
defaultExchange: defaultVenue,
|
|
817
|
+
defaultSlippageBps: slip.defaultSlippageBps,
|
|
818
|
+
slippageBps: slip.slippageBps,
|
|
291
819
|
});
|
|
820
|
+
await stepBuilder(p, venues, network, master.name, getPassphrase);
|
|
821
|
+
await stepSummary(venues, network, master.name, results, getPassphrase);
|
|
822
|
+
}
|
|
823
|
+
// ── Commands ──────────────────────────────────────────────────
|
|
824
|
+
function collect(value, prev) {
|
|
825
|
+
return [...prev, value];
|
|
292
826
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
827
|
+
export function registerInitCommand(program) {
|
|
828
|
+
program
|
|
829
|
+
.command("setup")
|
|
830
|
+
.alias("init")
|
|
831
|
+
.description("Setup wizard — OWS wallet, venues, agent wallets, slippage, default venue (scriptable with --non-interactive)")
|
|
832
|
+
.option("--non-interactive", "Run without prompts (scripts / CI)")
|
|
833
|
+
.option("--wallet <name>", "Existing OWS wallet to use as master")
|
|
834
|
+
.option("--wallet-name <name>", "Create a new OWS wallet with this name as master")
|
|
835
|
+
.addOption(deprecatedPassphraseOption("OWS vault passphrase")).hook("preAction", warnDeprecatedPassphrase)
|
|
836
|
+
.option("--venues <list>", "Comma-separated trading venues to configure")
|
|
837
|
+
.option("--default-venue <venue>", "Default venue (settings.defaultExchange)")
|
|
838
|
+
.option("--default-exchange <venue>", "Alias of --default-venue")
|
|
839
|
+
.option("--slippage-bps <n>", "Global default slippage bound in bps (settings.defaultSlippageBps)")
|
|
840
|
+
.option("--slippage <venue=bps>", "Per-venue slippage bound (repeatable)", collect, [])
|
|
841
|
+
.option("--approve-agents", "Register an agent wallet / API key on each --venues venue (requires --yes)")
|
|
842
|
+
.option("--yes", "Confirm signing for --approve-agents")
|
|
843
|
+
.option("--ondo-secret-stdin", "Read the Ondo API key id + secret from stdin (2 lines) for --approve-agents")
|
|
844
|
+
.option("--aster-can-spot", "Aster agent: also allow spot trading")
|
|
845
|
+
.option("--lighter-api-key-index <n>", "Lighter agent API key slot (4-254; default: next free)")
|
|
846
|
+
.option("--lighter-rh-api-key-index <n>", "Lighter RH agent API key slot (4-254 except 157; default: next free)")
|
|
847
|
+
.option("--arcus-account-index <n>", "Arcus subaccount for the API key: 0-9, or 255 for all")
|
|
848
|
+
.option("--import-agents <venues>", "Import keys created on the exchange WEBSITES for these --venues (requires --yes); keys on stdin, one line per venue in this order (ondo: 2 lines, key id then secret; qfex: 2 lines, public key then secret). No master wallet needed when every venue is imported")
|
|
849
|
+
.option("--master-address <venue=address>", "Master / owner public address for an --import-agents venue (repeatable; required for aster, pacifica, arcus)", collect, [])
|
|
850
|
+
.option("--lighter-account-index <n>", "Lighter account index of the imported API key (--import-agents)")
|
|
851
|
+
.option("--lighter-rh-account-index <n>", "Lighter RH account index of the imported API key (--import-agents)")
|
|
852
|
+
.option("--qfex-account-id <uuid>", "QFEX account the imported key trades (--import-agents; required — it confirms the account the key is bound to)")
|
|
853
|
+
.option("--allow-unverified <venues>", "Accept storing these --import-agents keys UNVERIFIED (required for pacifica: no read-only check exists); rolled back if another import in the batch fails")
|
|
854
|
+
.action(async (opts, command) => {
|
|
855
|
+
// optsWithGlobals() merges parent + subcommand options so --passphrase /
|
|
856
|
+
// --wallet work whether placed on `perp --passphrase X setup` (parent) or
|
|
857
|
+
// `perp setup --passphrase X` (subcommand). Commander v13 binds an
|
|
858
|
+
// identically named flag to the parent, so opts.<flag> alone would be
|
|
859
|
+
// undefined. The network is the root -n/--network flag (validated).
|
|
860
|
+
const merged = command.optsWithGlobals();
|
|
861
|
+
const resolvedOpts = {
|
|
862
|
+
...opts,
|
|
863
|
+
passphrase: merged.passphrase ?? opts.passphrase,
|
|
864
|
+
network: networkOf(command),
|
|
865
|
+
wallet: merged.wallet ?? opts.wallet,
|
|
866
|
+
};
|
|
867
|
+
if (resolvedOpts.nonInteractive) {
|
|
868
|
+
return runNonInteractiveSetup(resolvedOpts);
|
|
869
|
+
}
|
|
870
|
+
if (process.stdin.isTTY !== true) {
|
|
871
|
+
const err = new PerpError("INVALID_PARAMS", "Interactive setup needs a terminal", {
|
|
872
|
+
remediation: "Run in a terminal, or use: perp setup --non-interactive --wallet <name> --venues <list> ...",
|
|
873
|
+
});
|
|
874
|
+
console.error(chalk.red(`\n Error [${err.structured.code}]: ${err.structured.message}`));
|
|
875
|
+
console.error(chalk.gray(` Remediation: ${err.structured.remediation}\n`));
|
|
876
|
+
process.exitCode = 1;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
await runInteractiveSetup(ttyPrompter(), resolvedOpts.passphrase, resolvedOpts.network);
|
|
880
|
+
});
|
|
312
881
|
}
|
|
313
882
|
// ── Non-interactive setup (agent / CI / Docker) ──────────────────────────────
|
|
314
|
-
const VALID_EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
315
883
|
/**
|
|
316
884
|
* Test seam: emit JSON envelopes when the global --json flag is present.
|
|
317
885
|
* Mirrors the inline pattern used elsewhere (see src/index.ts:87) instead of
|
|
@@ -320,79 +888,373 @@ const VALID_EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
|
320
888
|
function setupIsJson() {
|
|
321
889
|
return process.argv.includes("--json");
|
|
322
890
|
}
|
|
891
|
+
/** Parse `<venue>=<address>` entries (--master-address). */
|
|
892
|
+
function parseVenueAddresses(entries, network) {
|
|
893
|
+
const out = {};
|
|
894
|
+
for (const entry of entries) {
|
|
895
|
+
const eq = entry.indexOf("=");
|
|
896
|
+
if (eq <= 0 || eq === entry.length - 1) {
|
|
897
|
+
throw new PerpError("INVALID_PARAMS", `Invalid --master-address "${entry}"`, { remediation: "Use <venue>=<address>, e.g. --master-address aster=0xYOU" });
|
|
898
|
+
}
|
|
899
|
+
out[resolveSetupVenue(entry.slice(0, eq), network)] = entry.slice(eq + 1).trim();
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
/** All of piped stdin (keys for --import-agents). */
|
|
904
|
+
function readSetupStdin() {
|
|
905
|
+
return new Promise((resolveRead, reject) => {
|
|
906
|
+
const chunks = [];
|
|
907
|
+
process.stdin.setEncoding("utf-8");
|
|
908
|
+
process.stdin.on("data", (c) => chunks.push(typeof c === "string" ? c : c.toString("utf-8")));
|
|
909
|
+
process.stdin.on("end", () => resolveRead(chunks.join("")));
|
|
910
|
+
process.stdin.on("error", reject);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function parseSlotFlag(flag, raw) {
|
|
914
|
+
if (raw === undefined)
|
|
915
|
+
return null;
|
|
916
|
+
const n = Number(raw);
|
|
917
|
+
if (!Number.isInteger(n))
|
|
918
|
+
throw new PerpError("INVALID_PARAMS", `${flag} must be an integer (got "${raw}")`);
|
|
919
|
+
return n;
|
|
920
|
+
}
|
|
323
921
|
/**
|
|
324
|
-
* Drive `perp setup` end-to-end without TTY.
|
|
325
|
-
* - --passphrase or OWS_PASSPHRASE env (3-path resolver)
|
|
326
|
-
* Optional:
|
|
327
|
-
* - --wallet-name (default: "main")
|
|
328
|
-
* - --default-exchange (must be one of pacifica/hyperliquid/lighter/aster)
|
|
922
|
+
* Drive `perp setup` end-to-end without TTY.
|
|
329
923
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
924
|
+
* Master wallet: exactly one of --wallet <existing> or --wallet-name <new>.
|
|
925
|
+
* Passphrase (--passphrase / OWS_PASSPHRASE / stdin) is required to create a
|
|
926
|
+
* wallet or to sign agent approvals. With --ondo-secret-stdin, stdin carries
|
|
927
|
+
* the Ondo key, so the passphrase must come from the flag or env.
|
|
333
928
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
929
|
+
* Side effects, in order: validate everything → create wallet (if asked) →
|
|
930
|
+
* write settings (owsActiveWallet, defaultExchange, slippage) → register
|
|
931
|
+
* agents (only with --approve-agents --yes; a failure on one venue does not
|
|
932
|
+
* stop the others). Never deposits, withdraws, or trades; never writes .env.
|
|
336
933
|
*/
|
|
337
934
|
export async function runNonInteractiveSetup(opts) {
|
|
338
935
|
const isJson = setupIsJson();
|
|
339
936
|
try {
|
|
340
|
-
// Validate
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
throw new PerpError("INVALID_PARAMS",
|
|
345
|
-
|
|
937
|
+
// ── Validate all inputs before any side effect ──
|
|
938
|
+
const network = parseNetwork(opts.network);
|
|
939
|
+
const venues = opts.venues !== undefined ? parseVenueList(opts.venues, network) : [];
|
|
940
|
+
if (opts.defaultVenue !== undefined && opts.defaultExchange !== undefined && opts.defaultVenue !== opts.defaultExchange) {
|
|
941
|
+
throw new PerpError("INVALID_PARAMS", "--default-venue and --default-exchange disagree", { remediation: "Pass only --default-venue" });
|
|
942
|
+
}
|
|
943
|
+
const defaultRaw = opts.defaultVenue !== undefined ? opts.defaultVenue : opts.defaultExchange;
|
|
944
|
+
let defaultVenue = null;
|
|
945
|
+
if (defaultRaw !== undefined) {
|
|
946
|
+
const id = tryResolveVenueId(defaultRaw);
|
|
947
|
+
if (id === null || !getVenue(id).roles.includes("trading")) {
|
|
948
|
+
throw new PerpError("INVALID_PARAMS", `Invalid --default-venue "${defaultRaw}"`, {
|
|
949
|
+
remediation: `Use one of: ${(venues.length > 0 ? venues : setupVenues().map(v => v.id)).join(", ")}`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (venues.length > 0 && !venues.includes(id)) {
|
|
953
|
+
throw new PerpError("INVALID_PARAMS", `--default-venue ${id} is not in --venues`, { remediation: `Use one of: ${venues.join(", ")}` });
|
|
954
|
+
}
|
|
955
|
+
defaultVenue = id;
|
|
956
|
+
}
|
|
957
|
+
else if (venues.length === 1) {
|
|
958
|
+
defaultVenue = venues[0];
|
|
959
|
+
}
|
|
960
|
+
const defaultSlippageBps = opts.slippageBps !== undefined
|
|
961
|
+
? parseSlippageSettingBps("defaultSlippageBps", opts.slippageBps, maxDefaultSlippageBps())
|
|
962
|
+
: null;
|
|
963
|
+
const venueSlippage = parseVenueSlippage(opts.slippage, network);
|
|
964
|
+
if (opts.approveAgents && !opts.yes) {
|
|
965
|
+
throw new PerpError("INVALID_PARAMS", "--approve-agents signs approvals with your master wallet and needs --yes", {
|
|
966
|
+
remediation: "Re-run with --approve-agents --yes",
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
if (opts.approveAgents && venues.length === 0) {
|
|
970
|
+
throw new PerpError("INVALID_PARAMS", "--approve-agents needs --venues", { remediation: "Pass --venues <list>" });
|
|
971
|
+
}
|
|
972
|
+
// Ondo in --import-agents is read from the shared stdin instead of --ondo-secret-stdin.
|
|
973
|
+
const ondoImported = opts.importAgents !== undefined && parseVenueList(opts.importAgents, network).includes("ondo");
|
|
974
|
+
const approvingOndo = opts.approveAgents === true && venues.includes("ondo") && !ondoImported;
|
|
975
|
+
if (opts.ondoSecretStdin && !approvingOndo) {
|
|
976
|
+
throw new PerpError("INVALID_PARAMS", "--ondo-secret-stdin is only used with --approve-agents and ondo in --venues", {
|
|
977
|
+
remediation: "Add --approve-agents --yes --venues ondo,... or drop --ondo-secret-stdin",
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (approvingOndo && !opts.ondoSecretStdin) {
|
|
981
|
+
throw new PerpError("INVALID_PARAMS", "Ondo agent registration reads the API key from stdin", {
|
|
982
|
+
remediation: "printf '%s\\n%s\\n' \"$KEY_ID\" \"$SECRET\" | perp setup --non-interactive ... --ondo-secret-stdin",
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
// Raw slots are what an --import-agents key uses (the website picked them); an approve slot must be 4-254.
|
|
986
|
+
const lighterSlotRaw = parseSlotFlag("--lighter-api-key-index", opts.lighterApiKeyIndex);
|
|
987
|
+
const lighterRhSlotRaw = parseSlotFlag("--lighter-rh-api-key-index", opts.lighterRhApiKeyIndex);
|
|
988
|
+
const importsVenue = (v) => opts.importAgents !== undefined && parseVenueList(opts.importAgents, network).includes(v);
|
|
989
|
+
const lighterSlot = importsVenue("lighter") ? null : lighterSlotRaw;
|
|
990
|
+
if (lighterSlot !== null)
|
|
991
|
+
assertLighterSlot("lighter", lighterSlot);
|
|
992
|
+
const lighterRhSlot = importsVenue("lighter-rh") ? null : lighterRhSlotRaw;
|
|
993
|
+
if (lighterRhSlot !== null)
|
|
994
|
+
assertLighterSlot("lighter-rh", lighterRhSlot);
|
|
995
|
+
const arcusAccountIndex = parseSlotFlag("--arcus-account-index", opts.arcusAccountIndex);
|
|
996
|
+
if (arcusAccountIndex !== null && !((arcusAccountIndex >= 0 && arcusAccountIndex <= 9) || arcusAccountIndex === 255)) {
|
|
997
|
+
throw new PerpError("INVALID_PARAMS", `--arcus-account-index must be 0-9 or 255 (got ${arcusAccountIndex})`);
|
|
998
|
+
}
|
|
999
|
+
// ── Website-created keys (--import-agents): validated here, keys read from stdin below ──
|
|
1000
|
+
const importVenues = opts.importAgents !== undefined ? parseVenueList(opts.importAgents, network) : [];
|
|
1001
|
+
if (importVenues.length > 0) {
|
|
1002
|
+
if (!opts.yes) {
|
|
1003
|
+
throw new PerpError("INVALID_PARAMS", "--import-agents stores and live-checks the given keys and needs --yes", { remediation: "Re-run with --import-agents <venues> --yes" });
|
|
1004
|
+
}
|
|
1005
|
+
const outside = importVenues.filter(v => !venues.includes(v));
|
|
1006
|
+
if (outside.length > 0) {
|
|
1007
|
+
throw new PerpError("INVALID_PARAMS", `--import-agents venue(s) not in --venues: ${outside.join(", ")}`, { remediation: "List every imported venue in --venues too" });
|
|
1008
|
+
}
|
|
1009
|
+
if (opts.ondoSecretStdin) {
|
|
1010
|
+
throw new PerpError("INVALID_PARAMS", "--ondo-secret-stdin cannot be combined with --import-agents (both read stdin)", {
|
|
1011
|
+
remediation: "Put ondo in --import-agents (its key id + secret are its 2 stdin lines)",
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (process.stdin.isTTY === true) {
|
|
1015
|
+
throw new PerpError("INVALID_PARAMS", "--import-agents reads the keys from stdin, but stdin is a terminal", {
|
|
1016
|
+
remediation: "Pipe the keys, one line per --import-agents venue in order (ondo: key id, secret): printf '%s\\n' \"$HL_AGENT_KEY\" \"$LT_KEY\" | perp setup --non-interactive ... --import-agents hyperliquid,lighter --yes",
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// M3: a venue whose key cannot be checked is stored only with explicit consent.
|
|
1021
|
+
const allowUnverified = opts.allowUnverified !== undefined ? parseVenueList(opts.allowUnverified, network) : [];
|
|
1022
|
+
const strayAllow = allowUnverified.filter(v => !importVenues.includes(v));
|
|
1023
|
+
if (strayAllow.length > 0) {
|
|
1024
|
+
throw new PerpError("INVALID_PARAMS", `--allow-unverified ${strayAllow.join(",")}: not in --import-agents`, { remediation: "List only --import-agents venues in --allow-unverified" });
|
|
1025
|
+
}
|
|
1026
|
+
const unverifiedNotAllowed = importVenues.filter(v => UNVERIFIED_IMPORT_VENUES.includes(v) && !allowUnverified.includes(v));
|
|
1027
|
+
if (unverifiedNotAllowed.length > 0) {
|
|
1028
|
+
throw new PerpError("INVALID_PARAMS", `--import-agents ${unverifiedNotAllowed.join(",")}: the key cannot be checked against the venue and would be stored UNVERIFIED`, {
|
|
1029
|
+
remediation: `Accept that explicitly with --allow-unverified ${unverifiedNotAllowed.join(",")} (it is removed again if another import in the batch fails), or import it alone: perp wallet agent import ${unverifiedNotAllowed[0]}`,
|
|
346
1030
|
});
|
|
347
1031
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1032
|
+
const masterAddresses = parseVenueAddresses(opts.masterAddress, network);
|
|
1033
|
+
for (const v of Object.keys(masterAddresses)) {
|
|
1034
|
+
if (!importVenues.includes(v))
|
|
1035
|
+
throw new PerpError("INVALID_PARAMS", `--master-address ${v}=… is only used for --import-agents venues`);
|
|
1036
|
+
}
|
|
1037
|
+
const lighterAccount = parseSlotFlag("--lighter-account-index", opts.lighterAccountIndex);
|
|
1038
|
+
const lighterRhAccount = parseSlotFlag("--lighter-rh-account-index", opts.lighterRhAccountIndex);
|
|
1039
|
+
const qfexAccountId = opts.qfexAccountId === undefined ? null : opts.qfexAccountId;
|
|
1040
|
+
if (qfexAccountId !== null && !importVenues.includes("qfex")) {
|
|
1041
|
+
throw new PerpError("INVALID_PARAMS", "--qfex-account-id is only used with qfex in --import-agents");
|
|
1042
|
+
}
|
|
1043
|
+
if (importVenues.includes("qfex") && qfexAccountId === null) {
|
|
1044
|
+
throw new PerpError("INVALID_PARAMS", "--import-agents qfex needs --qfex-account-id <uuid>: the account the key trades must be named explicitly", {
|
|
1045
|
+
remediation: "Find the account id in the QFEX web app (or run `perp wallet agent import qfex` in a terminal, which shows it for confirmation)",
|
|
355
1046
|
});
|
|
356
1047
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
1048
|
+
const approveVenues = opts.approveAgents === true ? venues.filter(v => !importVenues.includes(v)) : [];
|
|
1049
|
+
if (opts.wallet !== undefined && opts.walletName !== undefined) {
|
|
1050
|
+
throw new PerpError("INVALID_PARAMS", "Pass either --wallet <existing> or --wallet-name <new>, not both");
|
|
1051
|
+
}
|
|
1052
|
+
// No master wallet on this computer is fine when every agent comes from the website.
|
|
1053
|
+
const noMaster = opts.wallet === undefined && opts.walletName === undefined;
|
|
1054
|
+
if (noMaster && (importVenues.length === 0 || approveVenues.length > 0)) {
|
|
1055
|
+
throw new PerpError("INVALID_PARAMS", "No master wallet given", {
|
|
1056
|
+
remediation: "Pass --wallet <existing OWS wallet> (see: perp wallet list) or --wallet-name <new name> — or, without a master on this computer, import website-created keys for every venue: --import-agents <venues> --yes",
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
// ── Passphrase: needed to create a wallet or sign approvals (Ondo / imports sign nothing) ──
|
|
1060
|
+
const needsPassphrase = opts.walletName !== undefined || approveVenues.some(v => v !== "ondo");
|
|
1061
|
+
let passphrase = null;
|
|
1062
|
+
if (needsPassphrase) {
|
|
1063
|
+
if ((opts.ondoSecretStdin || importVenues.length > 0) && opts.passphrase === undefined && !("OWS_PASSPHRASE" in process.env)) {
|
|
1064
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "stdin carries the keys, so the passphrase must come from --passphrase or OWS_PASSPHRASE", {
|
|
1065
|
+
remediation: "Set OWS_PASSPHRASE or pass --passphrase",
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
// resolvePassphrase already throws PASSPHRASE_REQUIRED when stdin is
|
|
1069
|
+
// non-TTY and nothing was provided. In TTY mode it returns null —
|
|
1070
|
+
// non-interactive mode must NOT fall back to a prompt.
|
|
1071
|
+
passphrase = await resolvePassphrase({ flag: opts.passphrase });
|
|
1072
|
+
if (passphrase === null) {
|
|
1073
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "Non-interactive setup requires a passphrase", {
|
|
1074
|
+
remediation: "Provide --passphrase <pp> or set OWS_PASSPHRASE env var",
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// ── Imported keys: read all stdin lines now (before any side effect) ──
|
|
1079
|
+
const importSecrets = new Map();
|
|
1080
|
+
if (importVenues.length > 0) {
|
|
1081
|
+
const lines = (await readSetupStdin()).split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
|
1082
|
+
const want = importVenues.reduce((n, v) => n + importSecretLineCount(v), 0);
|
|
1083
|
+
if (lines.length !== want) {
|
|
1084
|
+
throw new PerpError("INVALID_PARAMS", `--import-agents ${importVenues.join(",")} expects ${want} line(s) on stdin (one per venue in order; ondo: key id, then secret; qfex: public key, then secret); got ${lines.length}`, {
|
|
1085
|
+
remediation: "Pipe exactly one key line per venue, in --import-agents order",
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
let at = 0;
|
|
1089
|
+
for (const v of importVenues) {
|
|
1090
|
+
const n = importSecretLineCount(v);
|
|
1091
|
+
importSecrets.set(v, importSecretFromLines(v, lines.slice(at, at + n)));
|
|
1092
|
+
at += n;
|
|
1093
|
+
}
|
|
1094
|
+
assertDistinctImportSecrets([...importSecrets.entries()]);
|
|
1095
|
+
}
|
|
1096
|
+
// ── Master wallet ──
|
|
360
1097
|
const ows = loadOws();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1098
|
+
let wallet;
|
|
1099
|
+
let created;
|
|
1100
|
+
if (noMaster) {
|
|
1101
|
+
wallet = null;
|
|
1102
|
+
created = false;
|
|
1103
|
+
}
|
|
1104
|
+
else if (opts.walletName !== undefined) {
|
|
1105
|
+
wallet = ows.createWallet(opts.walletName, passphrase);
|
|
1106
|
+
created = true;
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
const name = opts.wallet;
|
|
1110
|
+
const found = ows.listWallets().find(w => w.name === name);
|
|
1111
|
+
if (!found) {
|
|
1112
|
+
throw new PerpError("KEY_NOT_FOUND", `OWS wallet "${name}" not found`, {
|
|
1113
|
+
remediation: "List wallets with: perp wallet list (or create one with --wallet-name <name>)",
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
wallet = found;
|
|
1117
|
+
created = false;
|
|
366
1118
|
}
|
|
367
|
-
|
|
368
|
-
|
|
1119
|
+
// ── Settings ──
|
|
1120
|
+
const settings = applySetupSettings({
|
|
1121
|
+
owsActiveWallet: wallet !== null ? wallet.name : null,
|
|
1122
|
+
defaultExchange: defaultVenue,
|
|
1123
|
+
defaultSlippageBps,
|
|
1124
|
+
slippageBps: venueSlippage,
|
|
1125
|
+
});
|
|
1126
|
+
// ── Agents ──
|
|
1127
|
+
const agents = [];
|
|
1128
|
+
for (const venue of importVenues) {
|
|
1129
|
+
try {
|
|
1130
|
+
const outcome = await importVenueAgent({
|
|
1131
|
+
venue, network, agentName: null,
|
|
1132
|
+
masterAddress: masterAddresses[venue] ?? null,
|
|
1133
|
+
accountIndex: venue === "lighter" ? lighterAccount : venue === "lighter-rh" ? lighterRhAccount : venue === "arcus" ? arcusAccountIndex : null,
|
|
1134
|
+
apiKeyIndex: venue === "lighter" ? lighterSlotRaw : venue === "lighter-rh" ? lighterRhSlotRaw : null,
|
|
1135
|
+
expiresIn: null, replace: false,
|
|
1136
|
+
secret: importSecrets.get(venue),
|
|
1137
|
+
accountId: venue === "qfex" ? qfexAccountId : null,
|
|
1138
|
+
// Non-interactive: only the explicitly named account counts as confirmed.
|
|
1139
|
+
confirmAccount: venue !== "qfex" ? null : async (binding) => {
|
|
1140
|
+
if (qfexAccountId !== null && qfexAccountId.toLowerCase() === binding.accountId)
|
|
1141
|
+
return true;
|
|
1142
|
+
throw new PerpError("INVALID_PARAMS", `QFEX key resolves to account ${binding.accountId}, not --qfex-account-id ${String(qfexAccountId)}`, {
|
|
1143
|
+
remediation: `Re-run with --qfex-account-id ${binding.accountId} if that is the account you intend to trade`,
|
|
1144
|
+
});
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
agents.push({ venue, status: "imported", agentName: outcome.agentName, verification: outcome.verification });
|
|
1148
|
+
}
|
|
1149
|
+
catch (err) {
|
|
1150
|
+
agents.push(failed(venue, err));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// M3: one failed import → the batch's UNVERIFIED imports are removed again (never left behind
|
|
1154
|
+
// unchecked while the user is fixing the batch).
|
|
1155
|
+
const failedImports = agents.filter(a => a.status === "failed").map(a => a.venue);
|
|
1156
|
+
if (failedImports.length > 0) {
|
|
1157
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1158
|
+
const a = agents[i];
|
|
1159
|
+
if (a.status !== "imported" || a.verification.status !== "unverified")
|
|
1160
|
+
continue;
|
|
1161
|
+
const meta = getAgent(a.venue, a.agentName);
|
|
1162
|
+
if (meta !== null)
|
|
1163
|
+
await deleteImportedAgentLocal(a.venue, network, meta);
|
|
1164
|
+
agents[i] = failed(a.venue, new PerpError("APPROVE_FAILED", `${a.venue} key "${a.agentName}" was stored UNVERIFIED and has been removed again because ${failedImports.join(", ")} failed in the same batch`, {
|
|
1165
|
+
remediation: `Re-run the whole batch once ${failedImports.join(", ")} is fixed`,
|
|
1166
|
+
}));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (wallet !== null && approveVenues.length > 0) {
|
|
1170
|
+
for (const venue of approveVenues) {
|
|
1171
|
+
const blocker = approveBlocker(venue, network);
|
|
1172
|
+
if (blocker !== null) {
|
|
1173
|
+
agents.push({ venue, status: "skipped", reason: blocker });
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
const outcome = await approveVenueAgent({
|
|
1178
|
+
venue, network, master: wallet.name, agentName: null, expiresIn: "90d",
|
|
1179
|
+
canPerp: true, canSpot: venue === "aster" && opts.asterCanSpot === true, canWithdraw: false,
|
|
1180
|
+
rotate: false, passphrase,
|
|
1181
|
+
apiKeyIndex: venue === "lighter" ? lighterSlot : venue === "lighter-rh" ? lighterRhSlot : null,
|
|
1182
|
+
forceSlot: false,
|
|
1183
|
+
accountIndex: arcusAccountIndex !== null ? arcusAccountIndex : 0,
|
|
1184
|
+
aster: {},
|
|
1185
|
+
ondoKey: null, // venue === "ondo" → read from stdin (--ondo-secret-stdin)
|
|
1186
|
+
});
|
|
1187
|
+
agents.push({ venue, status: "approved", agentName: "meta" in outcome ? outcome.meta.agentName : outcome.agentName });
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
agents.push(failed(venue, err));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const data = {
|
|
1195
|
+
mode: "non-interactive",
|
|
1196
|
+
walletName: wallet !== null ? wallet.name : null,
|
|
1197
|
+
walletId: wallet !== null ? wallet.id : null,
|
|
1198
|
+
walletCreated: created,
|
|
1199
|
+
accounts: wallet !== null ? wallet.accounts : [],
|
|
1200
|
+
owsActiveWallet: settings.owsActiveWallet,
|
|
1201
|
+
network,
|
|
1202
|
+
venues,
|
|
1203
|
+
defaultExchange: settings.defaultExchange === "" ? null : settings.defaultExchange,
|
|
1204
|
+
defaultSlippageBps: settings.defaultSlippageBps,
|
|
1205
|
+
slippageBps: settings.slippageBps,
|
|
1206
|
+
agents,
|
|
1207
|
+
warnings: agents.some(a => a.status === "imported") ? [IMPORT_KEY_WARNING] : [],
|
|
1208
|
+
legacyEnvKeys: legacyEnvKeyNames(),
|
|
1209
|
+
nextSteps: nextStepCommands(venues, network),
|
|
1210
|
+
};
|
|
1211
|
+
const failures = agents.filter(a => a.status === "failed");
|
|
1212
|
+
if (failures.length > 0) {
|
|
1213
|
+
throw new PerpError("APPROVE_FAILED", `Agent registration failed for: ${failures.map(f => f.venue).join(", ")} (settings were saved)`, {
|
|
1214
|
+
result: data,
|
|
1215
|
+
remediation: `Fix the reported error, then: perp wallet agent ${failures.every(f => importVenues.includes(f.venue)) ? "import" : "approve"} <venue>`,
|
|
1216
|
+
});
|
|
369
1217
|
}
|
|
370
|
-
saveSettings(settings);
|
|
371
1218
|
if (isJson) {
|
|
372
|
-
printJson(jsonOk(
|
|
373
|
-
walletName: w.name,
|
|
374
|
-
walletId: w.id,
|
|
375
|
-
accounts: w.accounts,
|
|
376
|
-
owsActiveWallet: settings.owsActiveWallet,
|
|
377
|
-
defaultExchange: settings.defaultExchange ?? null,
|
|
378
|
-
mode: "non-interactive",
|
|
379
|
-
}));
|
|
1219
|
+
printJson(jsonOk(data));
|
|
380
1220
|
return;
|
|
381
1221
|
}
|
|
382
1222
|
console.log(chalk.cyan.bold("\n perp-cli Setup (non-interactive) — Done\n"));
|
|
383
|
-
console.log(` Wallet: ${chalk.white.bold(
|
|
1223
|
+
console.log(` Wallet: ${wallet !== null ? `${chalk.white.bold(wallet.name)}${created ? chalk.gray(" (created)") : ""}` : chalk.gray("none on this computer (imported keys only)")}`);
|
|
384
1224
|
console.log(` Active wallet: ${chalk.cyan(settings.owsActiveWallet)}`);
|
|
385
|
-
|
|
1225
|
+
console.log(` Network: ${network}`);
|
|
1226
|
+
if (venues.length > 0)
|
|
1227
|
+
console.log(` Venues: ${venues.join(", ")}`);
|
|
1228
|
+
if (settings.defaultExchange)
|
|
386
1229
|
console.log(` Default exchange: ${chalk.cyan(settings.defaultExchange)}`);
|
|
1230
|
+
console.log(` Slippage: ${settings.defaultSlippageBps} bps default${Object.entries(settings.slippageBps).map(([v, b]) => `, ${v} ${b}`).join("")}`);
|
|
1231
|
+
for (const a of agents) {
|
|
1232
|
+
const text = a.status === "approved" ? chalk.green(`agent ${a.agentName}`)
|
|
1233
|
+
: a.status === "imported" ? (a.verification.status === "verified"
|
|
1234
|
+
? chalk.green(`imported ${a.agentName} (verified)`)
|
|
1235
|
+
: chalk.yellow(`imported ${a.agentName} — NOT verified: ${a.verification.method}`))
|
|
1236
|
+
: chalk.gray(`skipped — ${a.status === "skipped" ? a.reason : ""}`);
|
|
1237
|
+
console.log(` ${a.venue.padEnd(17)} ${text}`);
|
|
387
1238
|
}
|
|
388
|
-
|
|
1239
|
+
if (importVenues.length > 0) {
|
|
1240
|
+
console.log(chalk.gray(`\n ${IMPORT_SCOPE_NOTE}`));
|
|
1241
|
+
console.log(chalk.yellow(` [warn] ${IMPORT_KEY_WARNING}`));
|
|
1242
|
+
}
|
|
1243
|
+
if (data.legacyEnvKeys.length > 0) {
|
|
1244
|
+
console.log(chalk.yellow(`\n ⚠ ${ENV_FILE} holds raw private keys (${data.legacyEnvKeys.join(", ")}); consider moving them to the OWS vault (perp wallet import).`));
|
|
1245
|
+
}
|
|
1246
|
+
console.log(chalk.gray(`\n Vault: ~/.ows/ (AES-256-GCM encrypted)`));
|
|
1247
|
+
console.log(chalk.white.bold("\n Next steps:"));
|
|
1248
|
+
for (const c of data.nextSteps)
|
|
1249
|
+
console.log(` ${chalk.green(c)}`);
|
|
1250
|
+
console.log();
|
|
389
1251
|
}
|
|
390
1252
|
catch (err) {
|
|
391
1253
|
if (isJson) {
|
|
392
1254
|
if (err instanceof PerpError) {
|
|
393
1255
|
printJson(jsonError(err.structured.code, err.structured.message, {
|
|
394
|
-
...(err.structured.remediation
|
|
395
|
-
? { details: { remediation: err.structured.remediation } }
|
|
1256
|
+
...(err.structured.remediation || err.structured.details
|
|
1257
|
+
? { details: { ...err.structured.details, ...(err.structured.remediation ? { remediation: err.structured.remediation } : {}) } }
|
|
396
1258
|
: {}),
|
|
397
1259
|
}));
|
|
398
1260
|
}
|
|
@@ -408,6 +1270,11 @@ export async function runNonInteractiveSetup(opts) {
|
|
|
408
1270
|
if (err.structured.remediation) {
|
|
409
1271
|
console.error(chalk.gray(` Remediation: ${err.structured.remediation}\n`));
|
|
410
1272
|
}
|
|
1273
|
+
const result = err.structured.details?.result;
|
|
1274
|
+
for (const a of result?.agents ?? []) {
|
|
1275
|
+
if (a.status === "failed")
|
|
1276
|
+
console.error(chalk.red(` ${a.venue}: [${a.error.code}] ${a.error.message}`));
|
|
1277
|
+
}
|
|
411
1278
|
}
|
|
412
1279
|
else {
|
|
413
1280
|
const msg = err instanceof Error ? err.message : String(err);
|