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/wallet.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
import { evmOnchainBalances, solanaOnchainBalances, stableTotal } from "../onchain-balances.js";
|
|
3
|
+
import { PerpError } from "../errors.js";
|
|
1
4
|
import chalk from "chalk";
|
|
2
|
-
import
|
|
5
|
+
import bs58 from "bs58";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
7
|
+
import { writeKeystoreAtomic } from "../agent-wallet/venue-keystore.js";
|
|
3
8
|
import { resolve } from "path";
|
|
4
|
-
import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
|
|
5
|
-
import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile,
|
|
9
|
+
import { formatUsd, makeTable, printJson, jsonOk, withJsonErrors } from "../utils.js";
|
|
10
|
+
import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, ENV_FILE } from "./init.js";
|
|
11
|
+
import { scrubSecretEnv } from "../secret-env.js";
|
|
6
12
|
import { loadSettings, saveSettings } from "../settings.js";
|
|
7
|
-
import { registerWalletAgentCommands } from "./agent.js";
|
|
13
|
+
import { deprecatedPassphraseOption, registerWalletAgentCommands, warnDeprecatedPassphrase } from "./agent.js";
|
|
8
14
|
import { registerWalletManageCommands } from "./manage.js";
|
|
15
|
+
import { registerWalletBuilderCommands } from "./builder.js";
|
|
9
16
|
import { loadOws } from "../signer/ows-loader.js";
|
|
10
17
|
// ── OWS CLI subprocess helper (used by backup/restore/rotate) ──
|
|
11
18
|
//
|
|
@@ -41,7 +48,8 @@ async function runOwsCli(args, timeout = 30000) {
|
|
|
41
48
|
if (!owsBin) {
|
|
42
49
|
throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
|
|
43
50
|
}
|
|
44
|
-
|
|
51
|
+
// The OWS CLI child never receives key material (SSOT Rule #3): secret env vars are stripped.
|
|
52
|
+
const owsEnv = { ...scrubSecretEnv(process.env), PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
|
|
45
53
|
const proc = spawnSync(owsBin, args, { encoding: "utf-8", timeout, env: owsEnv });
|
|
46
54
|
if (proc.error)
|
|
47
55
|
throw new Error(`OWS CLI failed: ${proc.error.message}`);
|
|
@@ -66,7 +74,8 @@ function loadStore() {
|
|
|
66
74
|
}
|
|
67
75
|
function saveStore(store) {
|
|
68
76
|
ensurePerpDir();
|
|
69
|
-
|
|
77
|
+
// Atomic, explicit 0600 (writeFileSync's mode is ignored when the file already exists).
|
|
78
|
+
writeKeystoreAtomic(WALLETS_FILE, JSON.stringify(store, null, 2));
|
|
70
79
|
}
|
|
71
80
|
/** Exported for smart landing page in index.ts */
|
|
72
81
|
export function getWalletSetupStatus() {
|
|
@@ -90,80 +99,60 @@ export function getWalletKeyByName(name) {
|
|
|
90
99
|
const store = loadStore();
|
|
91
100
|
return store.wallets[name]?.privateKey ?? null;
|
|
92
101
|
}
|
|
102
|
+
/** SOL + USDC / USDT on Solana (devnet: USDP). */
|
|
93
103
|
async function getSolanaBalances(address, isTestnet) {
|
|
94
|
-
|
|
95
|
-
const rpcUrl = isTestnet
|
|
96
|
-
? "https://api.devnet.solana.com"
|
|
97
|
-
: "https://api.mainnet-beta.solana.com";
|
|
98
|
-
const connection = new Connection(rpcUrl);
|
|
99
|
-
const pubkey = new PublicKey(address);
|
|
100
|
-
const balances = [];
|
|
101
|
-
const solLamports = await connection.getBalance(pubkey);
|
|
102
|
-
const solBalance = solLamports / 1e9;
|
|
103
|
-
balances.push({ token: "SOL", balance: solBalance.toFixed(6), usdValue: "" });
|
|
104
|
-
const usdcMint = isTestnet
|
|
105
|
-
? "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM"
|
|
106
|
-
: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
107
|
-
try {
|
|
108
|
-
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(pubkey, {
|
|
109
|
-
mint: new PublicKey(usdcMint),
|
|
110
|
-
});
|
|
111
|
-
let usdcBalance = 0;
|
|
112
|
-
for (const { account } of tokenAccounts.value) {
|
|
113
|
-
const parsed = account.data.parsed?.info;
|
|
114
|
-
if (parsed)
|
|
115
|
-
usdcBalance += Number(parsed.tokenAmount?.uiAmount ?? 0);
|
|
116
|
-
}
|
|
117
|
-
balances.push({
|
|
118
|
-
token: isTestnet ? "USDP" : "USDC",
|
|
119
|
-
balance: usdcBalance.toFixed(2),
|
|
120
|
-
usdValue: `$${formatUsd(usdcBalance)}`,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
balances.push({ token: isTestnet ? "USDP" : "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd");
|
|
128
|
-
const json = (await res.json());
|
|
129
|
-
balances[0].usdValue = `$${formatUsd(solBalance * (json.solana?.usd ?? 0))}`;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
balances[0].usdValue = "-";
|
|
133
|
-
}
|
|
134
|
-
return balances;
|
|
104
|
+
return solanaOnchainBalances(address, isTestnet);
|
|
135
105
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
? "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
106
|
+
/** Gas token + USDC / USDT (USDG on Robinhood Chain) on every EVM chain perp-cli uses; `only` limits the chains. */
|
|
107
|
+
async function getEvmBalances(address, isTestnet, only) {
|
|
108
|
+
return evmOnchainBalances(address, isTestnet, only);
|
|
109
|
+
}
|
|
110
|
+
/** Human table: non-zero balances and failed reads (JSON keeps every row), then the stablecoin total. */
|
|
111
|
+
function balanceTable(balances) {
|
|
112
|
+
const shown = balances.filter(b => b.error !== null || Number(b.balance) !== 0);
|
|
113
|
+
const rows = shown.map((b) => [
|
|
114
|
+
b.chain,
|
|
115
|
+
chalk.white.bold(b.token),
|
|
116
|
+
b.balance === null ? chalk.red("read failed") : b.balance,
|
|
117
|
+
b.usdValue === null ? chalk.gray("-") : `$${formatUsd(b.usdValue)}`,
|
|
118
|
+
]);
|
|
119
|
+
const total = stableTotal(balances);
|
|
120
|
+
rows.push([chalk.gray("total"), chalk.white.bold("stablecoins"), "", `$${formatUsd(total.usd)}${total.errors.length > 0 ? chalk.red(" (some reads failed)") : ""}`]);
|
|
121
|
+
return makeTable(["Chain", "Token", "Balance", "USD Value"], rows);
|
|
122
|
+
}
|
|
123
|
+
async function balanceEntry(label, vm, address, testnet) {
|
|
124
|
+
const balances = vm === "solana" ? await getSolanaBalances(address, testnet) : await getEvmBalances(address, testnet);
|
|
125
|
+
const total = stableTotal(balances);
|
|
126
|
+
return { ...label, chain: vm, address, balances, stableUsd: total.usd, errors: total.errors };
|
|
127
|
+
}
|
|
128
|
+
/** Reads each distinct address once (several venues often share one key), in order. */
|
|
129
|
+
async function balanceEntries(items, testnet) {
|
|
130
|
+
const seen = new Map();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const it of items) {
|
|
133
|
+
const key = `${it.vm}:${it.address.toLowerCase()}`;
|
|
134
|
+
const prev = seen.get(key);
|
|
135
|
+
if (prev !== undefined) {
|
|
136
|
+
if (it.label.exchange !== undefined)
|
|
137
|
+
prev.exchange = prev.exchange === undefined ? it.label.exchange : `${prev.exchange},${it.label.exchange}`;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const entry = await balanceEntry(it.label, it.vm, it.address, testnet);
|
|
141
|
+
seen.set(key, entry);
|
|
142
|
+
out.push(entry);
|
|
162
143
|
}
|
|
163
|
-
|
|
164
|
-
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/** Exactly one JSON document (an array of entries), or one table per entry. */
|
|
147
|
+
function printBalanceEntries(entries, json) {
|
|
148
|
+
if (json)
|
|
149
|
+
return printJson(jsonOk(entries));
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const who = [e.wallet, e.exchange].filter(x => x !== undefined).join(" / ");
|
|
152
|
+
console.log(chalk.cyan.bold(`\n ${who} (${e.chain} ${e.address.slice(0, 8)}...)`));
|
|
153
|
+
console.log(balanceTable(e.balances));
|
|
165
154
|
}
|
|
166
|
-
|
|
155
|
+
console.log();
|
|
167
156
|
}
|
|
168
157
|
// ── Derive address from private key ──────────────────────────
|
|
169
158
|
async function deriveSolanaAddress(key) {
|
|
@@ -182,141 +171,114 @@ async function deriveEvmAddress(key) {
|
|
|
182
171
|
const pk = key.startsWith("0x") ? key : `0x${key}`;
|
|
183
172
|
return new ethers.Wallet(pk).address;
|
|
184
173
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log(chalk.yellow.bold("\n [Legacy] Solana Wallet Generated\n"));
|
|
207
|
-
console.log(` Name: ${chalk.white.bold(walletName)}`);
|
|
208
|
-
console.log(` Address: ${chalk.green(address)}`);
|
|
209
|
-
console.log(chalk.gray(` Saved: ~/.perp/.env + wallets.json (unencrypted)`));
|
|
210
|
-
console.log(chalk.yellow(`\n Consider: ${chalk.cyan("perp wallet migrate")} to move to OWS vault\n`));
|
|
174
|
+
/** `--legacy` (raw keys written to ~/.perp/.env + wallets.json) was removed — SSOT Rule #3. */
|
|
175
|
+
function legacyWriteRemoved(cmd) {
|
|
176
|
+
return new PerpError("INVALID_PARAMS", `wallet ${cmd} --legacy was removed: it wrote raw private keys to ~/.perp/.env and wallets.json`, {
|
|
177
|
+
remediation: cmd === "generate"
|
|
178
|
+
? "Run: perp wallet generate [name] (OWS encrypted vault). Existing ~/.perp/.env keys keep working; move them with perp wallet set <exchange>"
|
|
179
|
+
: "Run: perp wallet import --name <name> [--chain evm|solana|dual] (key via hidden prompt or stdin) or perp wallet set <exchange>",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
|
|
184
|
+
* exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
|
|
185
|
+
* base58/JSON-array/hex and normalize to 32-byte hex secret.
|
|
186
|
+
*/
|
|
187
|
+
export function normalizeSolanaKey(input) {
|
|
188
|
+
const s = input.trim();
|
|
189
|
+
// Already hex (with optional 0x prefix)
|
|
190
|
+
const hexNoPrefix = s.startsWith("0x") ? s.slice(2) : s;
|
|
191
|
+
if (/^[0-9a-fA-F]{64}$/.test(hexNoPrefix))
|
|
192
|
+
return hexNoPrefix;
|
|
193
|
+
if (/^[0-9a-fA-F]{128}$/.test(hexNoPrefix)) {
|
|
194
|
+
return hexNoPrefix.slice(0, 64); // 64-byte keypair → first 32 bytes secret
|
|
211
195
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (store.wallets[walletName]) {
|
|
218
|
-
console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
|
|
219
|
-
process.exit(1);
|
|
196
|
+
// JSON byte array
|
|
197
|
+
if (s.startsWith("[")) {
|
|
198
|
+
let arr;
|
|
199
|
+
try {
|
|
200
|
+
arr = JSON.parse(s);
|
|
220
201
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return printJson(jsonOk({ name: walletName, type: "evm", address: w.address, legacy: true }));
|
|
231
|
-
console.log(chalk.yellow.bold("\n [Legacy] EVM Wallet Generated\n"));
|
|
232
|
-
console.log(` Name: ${chalk.white.bold(walletName)}`);
|
|
233
|
-
console.log(` Address: ${chalk.green(w.address)}`);
|
|
234
|
-
console.log(chalk.gray(` Saved: ~/.perp/.env + wallets.json (unencrypted)`));
|
|
235
|
-
console.log(chalk.yellow(`\n Consider: ${chalk.cyan("perp wallet migrate")} to move to OWS vault\n`));
|
|
202
|
+
catch {
|
|
203
|
+
// Never surface the parser message: V8 echoes the input (the secret) in it.
|
|
204
|
+
throw new Error("Invalid Solana JSON byte array (not valid JSON)");
|
|
205
|
+
}
|
|
206
|
+
if (arr.length === 64)
|
|
207
|
+
return Buffer.from(arr.slice(0, 32)).toString("hex");
|
|
208
|
+
if (arr.length === 32)
|
|
209
|
+
return Buffer.from(arr).toString("hex");
|
|
210
|
+
throw new Error(`Invalid Solana JSON byte array length: ${arr.length} (expected 32 or 64)`);
|
|
236
211
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
212
|
+
// base58 (Phantom export — typically 87-88 chars for 64-byte keypair)
|
|
213
|
+
try {
|
|
214
|
+
const decoded = bs58.decode(s);
|
|
215
|
+
if (decoded.length === 64)
|
|
216
|
+
return Buffer.from(decoded.slice(0, 32)).toString("hex");
|
|
217
|
+
if (decoded.length === 32)
|
|
218
|
+
return Buffer.from(decoded).toString("hex");
|
|
219
|
+
throw new Error(`Invalid Solana base58 byte length: ${decoded.length} (expected 32 or 64)`);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
|
|
240
224
|
}
|
|
241
225
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
address = kp.publicKey.toBase58();
|
|
258
|
-
normalizedKey = bs58.encode(kp.secretKey);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
console.error(chalk.red("\n Invalid Solana private key.\n"));
|
|
263
|
-
process.exit(1);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const store = loadStore();
|
|
267
|
-
if (store.wallets[name]) {
|
|
268
|
-
console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
store.wallets[name] = { name, type: "solana", address, privateKey: normalizedKey, createdAt: new Date().toISOString() };
|
|
272
|
-
if (!store.active.pacifica)
|
|
273
|
-
store.active.pacifica = name;
|
|
274
|
-
saveStore(store);
|
|
275
|
-
setEnvVar("PACIFICA_PRIVATE_KEY", normalizedKey);
|
|
276
|
-
if (isJson())
|
|
277
|
-
return printJson(jsonOk({ name, type: "solana", address, legacy: true }));
|
|
278
|
-
console.log(chalk.yellow.bold("\n [Legacy] Solana Key Imported\n"));
|
|
279
|
-
console.log(` Name: ${chalk.white.bold(name)}`);
|
|
280
|
-
console.log(` Address: ${chalk.green(address)}\n`);
|
|
226
|
+
/**
|
|
227
|
+
* Import key material into the OWS vault as wallet `name` — the single import
|
|
228
|
+
* path shared by `wallet import` and `perp setup`. Never logs key material.
|
|
229
|
+
*/
|
|
230
|
+
/**
|
|
231
|
+
* Strip secret material from an error message before it is shown/logged: the exact secret
|
|
232
|
+
* strings plus any long hex / base58 / byte-array run (keys never reach output — Rule #3).
|
|
233
|
+
*/
|
|
234
|
+
export function redactSecrets(message, secrets) {
|
|
235
|
+
let out = message;
|
|
236
|
+
for (const sec of secrets) {
|
|
237
|
+
const t = sec.trim();
|
|
238
|
+
for (const v of [t, t.replace(/^0x/i, "")])
|
|
239
|
+
if (v.length >= 8)
|
|
240
|
+
out = out.split(v).join("[redacted]");
|
|
281
241
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (isJson())
|
|
308
|
-
return printJson(jsonOk({ name, type: "evm", address, legacy: true }));
|
|
309
|
-
console.log(chalk.yellow.bold("\n [Legacy] EVM Key Imported\n"));
|
|
310
|
-
console.log(` Name: ${chalk.white.bold(name)}`);
|
|
311
|
-
console.log(` Address: ${chalk.green(address)}\n`);
|
|
242
|
+
return out
|
|
243
|
+
.replace(/(0x)?[0-9a-fA-F]{32,}/g, "[redacted]")
|
|
244
|
+
.replace(/[1-9A-HJ-NP-Za-km-z]{40,}/g, "[redacted]")
|
|
245
|
+
.replace(/\[\s*\d{1,3}(\s*,\s*\d{1,3}){7,}[^\]]*\]?/g, "[redacted]");
|
|
246
|
+
}
|
|
247
|
+
/** Read all of a piped (non-TTY) stdin. */
|
|
248
|
+
function readAllStdin() {
|
|
249
|
+
return new Promise((resolveRead, reject) => {
|
|
250
|
+
const chunks = [];
|
|
251
|
+
process.stdin.setEncoding("utf-8");
|
|
252
|
+
process.stdin.on("data", (c) => chunks.push(typeof c === "string" ? c : c.toString("utf-8")));
|
|
253
|
+
process.stdin.on("end", () => resolveRead(chunks.join("")));
|
|
254
|
+
process.stdin.on("error", reject);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
export async function importOwsWallet(name, passphrase, input) {
|
|
258
|
+
const secrets = input.kind === "mnemonic" ? [input.mnemonic] : input.kind === "dual" ? [input.evm, input.solana] : [input.key];
|
|
259
|
+
try {
|
|
260
|
+
return importOwsWalletRaw(name, passphrase, input);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
+
throw new PerpError("INVALID_PARAMS", `Import failed: ${redactSecrets(msg, secrets)}`, {
|
|
265
|
+
remediation: "Check the key / mnemonic format and retry (the input is never echoed)",
|
|
266
|
+
});
|
|
312
267
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
268
|
+
}
|
|
269
|
+
function importOwsWalletRaw(name, passphrase, input) {
|
|
270
|
+
const ows = loadOws();
|
|
271
|
+
if (input.kind === "mnemonic")
|
|
272
|
+
return ows.importWalletMnemonic(name, input.mnemonic, passphrase);
|
|
273
|
+
if (input.kind === "dual") {
|
|
274
|
+
// Dual-curve: both explicit keys (OWS 1.3+ API — both must be present)
|
|
275
|
+
return ows.importWalletPrivateKey(name, "", // ignored when both secp256k1Key and ed25519Key are provided
|
|
276
|
+
passphrase, undefined, undefined, input.evm, normalizeSolanaKey(input.solana));
|
|
316
277
|
}
|
|
278
|
+
const key = input.chain === "solana" ? normalizeSolanaKey(input.key) : input.key;
|
|
279
|
+
return ows.importWalletPrivateKey(name, key, passphrase, undefined, input.chain);
|
|
317
280
|
}
|
|
318
|
-
|
|
319
|
-
export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter) {
|
|
281
|
+
export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter, getAdapterForExchange) {
|
|
320
282
|
const wallet = program.command("wallet").description("Wallet management & on-chain balances");
|
|
321
283
|
// ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
|
|
322
284
|
wallet
|
|
@@ -324,12 +286,12 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
324
286
|
.description("Generate a new wallet (OWS encrypted vault, multi-chain)")
|
|
325
287
|
.option("--words <count>", "Mnemonic word count (12 or 24)", "12")
|
|
326
288
|
.option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
|
|
327
|
-
.
|
|
328
|
-
.
|
|
289
|
+
.addOption(deprecatedPassphraseOption()).hook("preAction", warnDeprecatedPassphrase)
|
|
290
|
+
.addOption(new Option("--legacy <chain>", "Removed — refused (raw keys are never written to ~/.perp/.env)").hideHelp())
|
|
329
291
|
.action(async (name, opts, command) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return
|
|
292
|
+
if (opts.legacy !== undefined) {
|
|
293
|
+
await withJsonErrors(isJson(), async () => { throw legacyWriteRemoved("generate"); });
|
|
294
|
+
return;
|
|
333
295
|
}
|
|
334
296
|
const walletName = name || "default";
|
|
335
297
|
try {
|
|
@@ -399,128 +361,73 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
399
361
|
// Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
|
|
400
362
|
// exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
|
|
401
363
|
// base58/JSON-array/hex and normalize to 32-byte hex secret.
|
|
364
|
+
// SSOT Rule #3: keys are never accepted on argv (shell history / process list) —
|
|
365
|
+
// hidden TTY prompt or stdin pipe only, matching `wallet set`.
|
|
402
366
|
wallet
|
|
403
367
|
.command("import [privateKey]")
|
|
404
|
-
.description("Import a private key into OWS encrypted vault (
|
|
368
|
+
.description("Import a private key or mnemonic into the OWS encrypted vault (key via hidden prompt or stdin — never argv)")
|
|
405
369
|
.option("--name <name>", "Wallet alias name", "imported")
|
|
406
|
-
.option("--chain <chain>", "Key type
|
|
407
|
-
.option("--
|
|
408
|
-
.
|
|
409
|
-
.
|
|
410
|
-
.
|
|
411
|
-
.
|
|
412
|
-
.action(async (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
422
|
-
process.exit(1);
|
|
423
|
-
}
|
|
424
|
-
return _legacyImport(opts.legacy, privateKey, opts.name, isJson);
|
|
425
|
-
}
|
|
426
|
-
// Validation: input mode is exactly one of (positional | --evm/--solana flags | mnemonic)
|
|
427
|
-
const hasPositional = !!privateKey;
|
|
428
|
-
const hasFlagKeys = !!(opts.evm || opts.solana);
|
|
429
|
-
if (!hasPositional && !hasFlagKeys) {
|
|
430
|
-
const msg = "Provide a <privateKey> argument OR --evm/--solana flag(s)";
|
|
431
|
-
if (isJson()) {
|
|
432
|
-
const { jsonError } = await import("../utils.js");
|
|
433
|
-
return printJson(jsonError("OWS_ERROR", msg));
|
|
434
|
-
}
|
|
435
|
-
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
436
|
-
process.exit(1);
|
|
437
|
-
}
|
|
438
|
-
if (hasPositional && hasFlagKeys) {
|
|
439
|
-
const msg = "Cannot mix positional <privateKey> with --evm/--solana flags. Use one input style.";
|
|
440
|
-
if (isJson()) {
|
|
441
|
-
const { jsonError } = await import("../utils.js");
|
|
442
|
-
return printJson(jsonError("OWS_ERROR", msg));
|
|
370
|
+
.option("--chain <chain>", "Key type: evm (default), solana, or dual (EVM + Solana keys: two prompts, or two stdin lines EVM first)", "evm")
|
|
371
|
+
.option("--mnemonic", "Import a mnemonic phrase instead of a private key")
|
|
372
|
+
.addOption(deprecatedPassphraseOption()).hook("preAction", warnDeprecatedPassphrase)
|
|
373
|
+
.addOption(new Option("--evm <hex>", "Removed — refused (key on argv); use --chain evm|dual").hideHelp())
|
|
374
|
+
.addOption(new Option("--solana <key>", "Removed — refused (key on argv); use --chain solana|dual").hideHelp())
|
|
375
|
+
.addOption(new Option("--legacy <chain>", "Removed — refused (raw keys are never written to ~/.perp/.env)").hideHelp())
|
|
376
|
+
.action(async (argvKey, opts, command) => {
|
|
377
|
+
await withJsonErrors(isJson(), async () => {
|
|
378
|
+
if (opts.legacy !== undefined)
|
|
379
|
+
throw legacyWriteRemoved("import");
|
|
380
|
+
if (argvKey !== undefined || opts.evm !== undefined || opts.solana !== undefined) {
|
|
381
|
+
throw new PerpError("INVALID_PARAMS", "Refusing a private key / mnemonic on the command line: it is visible in shell history and the process list", {
|
|
382
|
+
remediation: "Enter it at the hidden prompt (perp wallet import --name <name> [--chain evm|solana|dual] [--mnemonic]) or pipe it on stdin (e.g. pbpaste | OWS_PASSPHRASE=... perp wallet import --name <name>). Clear the key from your shell history.",
|
|
383
|
+
});
|
|
443
384
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (opts.mnemonic && hasFlagKeys) {
|
|
448
|
-
const msg = "--mnemonic cannot be combined with --evm/--solana";
|
|
449
|
-
if (isJson()) {
|
|
450
|
-
const { jsonError } = await import("../utils.js");
|
|
451
|
-
return printJson(jsonError("OWS_ERROR", msg));
|
|
385
|
+
const chain = opts.chain;
|
|
386
|
+
if (chain !== "evm" && chain !== "solana" && chain !== "dual") {
|
|
387
|
+
throw new PerpError("INVALID_PARAMS", `Unknown --chain "${chain}"`, { remediation: "Use --chain evm, solana, or dual" });
|
|
452
388
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const ows = loadOws();
|
|
458
|
-
// Auto-normalize Solana keys: accept hex / base58 (Phantom) / JSON byte array
|
|
459
|
-
const bs58 = (await import("bs58")).default;
|
|
460
|
-
const normalizeSolana = (input) => {
|
|
461
|
-
const s = input.trim();
|
|
462
|
-
// Already hex (with optional 0x prefix)
|
|
463
|
-
const hexNoPrefix = s.startsWith("0x") ? s.slice(2) : s;
|
|
464
|
-
if (/^[0-9a-fA-F]{64}$/.test(hexNoPrefix))
|
|
465
|
-
return hexNoPrefix;
|
|
466
|
-
if (/^[0-9a-fA-F]{128}$/.test(hexNoPrefix)) {
|
|
467
|
-
return hexNoPrefix.slice(0, 64); // 64-byte keypair → first 32 bytes secret
|
|
468
|
-
}
|
|
469
|
-
// JSON byte array
|
|
470
|
-
if (s.startsWith("[")) {
|
|
471
|
-
const arr = JSON.parse(s);
|
|
472
|
-
if (arr.length === 64)
|
|
473
|
-
return Buffer.from(arr.slice(0, 32)).toString("hex");
|
|
474
|
-
if (arr.length === 32)
|
|
475
|
-
return Buffer.from(arr).toString("hex");
|
|
476
|
-
throw new Error(`Invalid Solana JSON byte array length: ${arr.length} (expected 32 or 64)`);
|
|
477
|
-
}
|
|
478
|
-
// base58 (Phantom export — typically 87-88 chars for 64-byte keypair)
|
|
479
|
-
try {
|
|
480
|
-
const decoded = bs58.decode(s);
|
|
481
|
-
if (decoded.length === 64)
|
|
482
|
-
return Buffer.from(decoded.slice(0, 32)).toString("hex");
|
|
483
|
-
if (decoded.length === 32)
|
|
484
|
-
return Buffer.from(decoded).toString("hex");
|
|
485
|
-
throw new Error(`Invalid Solana base58 byte length: ${decoded.length} (expected 32 or 64)`);
|
|
486
|
-
}
|
|
487
|
-
catch (err) {
|
|
488
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
489
|
-
throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
// Resolve vault passphrase via the standard 3-path resolver (--passphrase
|
|
493
|
-
// > OWS_PASSPHRASE env > stdin pipe). When stdin is a TTY and nothing was
|
|
494
|
-
// provided, the resolver returns null; fall back to "" to preserve the
|
|
495
|
-
// historical "no passphrase" default for interactive use.
|
|
496
|
-
//
|
|
497
|
-
// optsWithGlobals() merges parent + subcommand options — see the
|
|
498
|
-
// wallet generate action for context. Without it, the parent program's
|
|
499
|
-
// --passphrase shadows the subcommand's same-named flag.
|
|
500
|
-
const merged = command.optsWithGlobals();
|
|
501
|
-
const { resolvePassphrase } = await import("../agent-wallet/passphrase.js");
|
|
502
|
-
const pp = (await resolvePassphrase({ flag: merged.passphrase ?? opts.passphrase })) ?? "";
|
|
503
|
-
let w;
|
|
504
|
-
if (opts.mnemonic) {
|
|
505
|
-
w = ows.importWalletMnemonic(opts.name, privateKey, pp);
|
|
389
|
+
if (opts.mnemonic && chain !== "evm") {
|
|
390
|
+
throw new PerpError("INVALID_PARAMS", "--mnemonic derives every chain itself and cannot be combined with --chain solana|dual", {
|
|
391
|
+
remediation: "Drop --chain: perp wallet import --mnemonic --name <name>",
|
|
392
|
+
});
|
|
506
393
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
394
|
+
const { promptHidden } = await import("./agent.js");
|
|
395
|
+
const piped = process.stdin.isTTY !== true;
|
|
396
|
+
const labels = opts.mnemonic ? ["mnemonic phrase"] : chain === "dual" ? ["EVM private key", "Solana private key"] : [`${chain === "evm" ? "EVM" : "Solana"} private key`];
|
|
397
|
+
let secrets;
|
|
398
|
+
if (piped) {
|
|
399
|
+
const lines = (await readAllStdin()).split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
|
400
|
+
secrets = opts.mnemonic ? [lines.join(" ")] : lines;
|
|
511
401
|
}
|
|
512
|
-
else
|
|
513
|
-
|
|
514
|
-
|
|
402
|
+
else {
|
|
403
|
+
secrets = [];
|
|
404
|
+
for (const label of labels)
|
|
405
|
+
secrets.push((await promptHidden(`${label} (hidden): `)).trim());
|
|
515
406
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
407
|
+
if (secrets.length !== labels.length || secrets.some(x => x.length === 0)) {
|
|
408
|
+
throw new PerpError("INVALID_PARAMS", `Expected ${labels.join(" + ")} on stdin / at the prompt (${labels.length === 2 ? "two lines, EVM first" : "one line"}); received ${secrets.filter(x => x.length > 0).length}`, {
|
|
409
|
+
remediation: "Enter it at the hidden prompt, or pipe it: pbpaste | OWS_PASSPHRASE=... perp wallet import --name <name>",
|
|
410
|
+
});
|
|
519
411
|
}
|
|
412
|
+
// Vault passphrase: --passphrase > OWS_PASSPHRASE > hidden prompt. Piped stdin
|
|
413
|
+
// carries the key, so a non-interactive run must name the passphrase explicitly.
|
|
414
|
+
// optsWithGlobals(): the root program's --passphrase shadows the subcommand's.
|
|
415
|
+
const flagPp = command.optsWithGlobals().passphrase;
|
|
416
|
+
let pp;
|
|
417
|
+
if (flagPp !== undefined)
|
|
418
|
+
pp = flagPp;
|
|
419
|
+
else if ("OWS_PASSPHRASE" in process.env)
|
|
420
|
+
pp = process.env.OWS_PASSPHRASE ?? "";
|
|
421
|
+
else if (!piped)
|
|
422
|
+
pp = await promptHidden("OWS vault passphrase (hidden, empty = none): ");
|
|
520
423
|
else {
|
|
521
|
-
|
|
522
|
-
|
|
424
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "stdin carries the key, so the vault passphrase must come from OWS_PASSPHRASE or --passphrase", {
|
|
425
|
+
remediation: `pbpaste | OWS_PASSPHRASE="$PP" perp wallet import --name ${opts.name} (use OWS_PASSPHRASE= for an unencrypted vault wallet)`,
|
|
426
|
+
});
|
|
523
427
|
}
|
|
428
|
+
const w = await importOwsWallet(opts.name, pp, opts.mnemonic ? { kind: "mnemonic", mnemonic: secrets[0] }
|
|
429
|
+
: chain === "dual" ? { kind: "dual", evm: secrets[0], solana: secrets[1] }
|
|
430
|
+
: { kind: "single", key: secrets[0], chain });
|
|
524
431
|
// Set as active wallet if none set
|
|
525
432
|
const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
|
|
526
433
|
const settings = ls();
|
|
@@ -545,16 +452,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
545
452
|
console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
|
|
546
453
|
}
|
|
547
454
|
console.log();
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
551
|
-
if (isJson()) {
|
|
552
|
-
const { jsonError } = await import("../utils.js");
|
|
553
|
-
return printJson(jsonError("OWS_ERROR", msg));
|
|
554
|
-
}
|
|
555
|
-
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
455
|
+
});
|
|
558
456
|
});
|
|
559
457
|
// ── use (set active wallet — OWS first, legacy fallback) ──
|
|
560
458
|
wallet
|
|
@@ -590,7 +488,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
590
488
|
if (!entry) {
|
|
591
489
|
console.error(chalk.red(`\n Wallet "${name}" not found in OWS vault or legacy store.\n`));
|
|
592
490
|
console.error(chalk.gray(` Create: perp wallet generate ${name}`));
|
|
593
|
-
console.error(chalk.gray(` Import: perp wallet import
|
|
491
|
+
console.error(chalk.gray(` Import: perp wallet import --name ${name} (hidden prompt / stdin)\n`));
|
|
594
492
|
process.exit(1);
|
|
595
493
|
}
|
|
596
494
|
const exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
|
|
@@ -604,83 +502,92 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
604
502
|
console.log(chalk.gray(` Address: ${entry.address}`));
|
|
605
503
|
console.log(chalk.yellow(` Note: consider migrating to OWS: perp wallet migrate\n`));
|
|
606
504
|
});
|
|
607
|
-
// ── set (
|
|
505
|
+
// ── set (import an exchange key into the OWS vault → active wallet) ──
|
|
506
|
+
//
|
|
507
|
+
// SSOT Rule #3: the key goes into the encrypted OWS vault, never ~/.perp/.env,
|
|
508
|
+
// and never via argv (shell history / process list). Input: hidden TTY prompt,
|
|
509
|
+
// or stdin pipe. Keys already in a legacy ~/.perp/.env keep working (read path unchanged).
|
|
608
510
|
wallet
|
|
609
|
-
.command("set <exchange>
|
|
610
|
-
.description("
|
|
511
|
+
.command("set <exchange> [key]")
|
|
512
|
+
.description("Import an exchange private key into the OWS vault and make it the active wallet (key via hidden prompt or stdin — never argv)")
|
|
513
|
+
.option("--name <name>", "OWS wallet name (default: the exchange id, e.g. hyperliquid)")
|
|
611
514
|
.option("--default", "Also set as default exchange")
|
|
612
|
-
.action(async (exchange,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if (isJson()) {
|
|
619
|
-
const { jsonError } = await import("../utils.js");
|
|
620
|
-
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast).`));
|
|
621
|
-
}
|
|
622
|
-
console.error(chalk.red(`\n Unknown exchange: ${exchange}`));
|
|
623
|
-
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast).\n`));
|
|
624
|
-
process.exit(1);
|
|
625
|
-
}
|
|
626
|
-
const { valid, address } = await validateKey(info.chain, key);
|
|
627
|
-
if (!valid) {
|
|
628
|
-
if (isJson()) {
|
|
629
|
-
const { jsonError } = await import("../utils.js");
|
|
630
|
-
return printJson(jsonError("INVALID_PARAMS", `Invalid ${info.chain} private key`));
|
|
515
|
+
.action(async (exchange, argvKey, opts, command) => {
|
|
516
|
+
await withJsonErrors(isJson(), async () => {
|
|
517
|
+
if (argvKey !== undefined) {
|
|
518
|
+
throw new PerpError("INVALID_PARAMS", "Refusing a private key on the command line: it is visible in shell history and the process list", {
|
|
519
|
+
remediation: `Enter it at the hidden prompt (perp wallet set ${exchange}) or pipe it on stdin (e.g. pbpaste | OWS_PASSPHRASE=... perp wallet set ${exchange}). Clear the key from your shell history.`,
|
|
520
|
+
});
|
|
631
521
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const settings = loadSettings();
|
|
641
|
-
settings.defaultExchange = resolved;
|
|
642
|
-
saveSettings(settings);
|
|
643
|
-
}
|
|
644
|
-
// Auto-setup Lighter API key if setting lighter PK
|
|
645
|
-
let lighterApiSetup = {};
|
|
646
|
-
if (resolved === "lighter") {
|
|
647
|
-
try {
|
|
648
|
-
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
649
|
-
const adapter = new LighterAdapter(normalized);
|
|
650
|
-
await adapter.init();
|
|
651
|
-
const apiKeyIndex = 4;
|
|
652
|
-
const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
|
|
653
|
-
// SSOT Rule #3: persist L2 key in encrypted keystore.
|
|
654
|
-
// accountIndex/apiKeyIndex live in settings.agents.lighter and don't
|
|
655
|
-
// need env duplication.
|
|
656
|
-
const { saveLighterKey } = await import("../agent-wallet/lighter-keystore.js");
|
|
657
|
-
saveLighterKey(adapter.accountIndex, apiKeyIndex, apiKey, "");
|
|
658
|
-
lighterApiSetup = { apiKey, accountIndex: adapter.accountIndex };
|
|
522
|
+
// Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter, ast → aster)
|
|
523
|
+
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
|
|
524
|
+
const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
|
|
525
|
+
const info = EXCHANGE_ENV_MAP[resolved];
|
|
526
|
+
if (!info) {
|
|
527
|
+
throw new PerpError("INVALID_PARAMS", `Unknown exchange: ${exchange}`, {
|
|
528
|
+
remediation: "Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast). Lighter RH / Ondo / Arcus use: perp wallet agent approve <lighter-rh|ondo|arcus>; QFEX: perp wallet agent import qfex.",
|
|
529
|
+
});
|
|
659
530
|
}
|
|
660
|
-
|
|
661
|
-
|
|
531
|
+
const { promptHidden } = await import("./agent.js");
|
|
532
|
+
const piped = process.stdin.isTTY !== true;
|
|
533
|
+
const key = (piped ? await readAllStdin() : await promptHidden(`${resolved} private key (hidden): `)).trim();
|
|
534
|
+
if (!key) {
|
|
535
|
+
throw new PerpError("INVALID_PARAMS", "No private key received", {
|
|
536
|
+
remediation: `Enter it at the hidden prompt, or pipe it: pbpaste | perp wallet set ${exchange}`,
|
|
537
|
+
});
|
|
662
538
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}));
|
|
669
|
-
console.log(chalk.green(`\n ${resolved} configured.`));
|
|
670
|
-
console.log(` Address: ${chalk.green(address)}`);
|
|
671
|
-
console.log(` Saved to: ${chalk.gray("~/.perp/.env")}`);
|
|
672
|
-
if (opts.default)
|
|
673
|
-
console.log(` Default: ${chalk.cyan(resolved)}`);
|
|
674
|
-
if (resolved === "lighter") {
|
|
675
|
-
if (lighterApiSetup.apiKey) {
|
|
676
|
-
console.log(chalk.green(` API Key: auto-registered (index: ${lighterApiSetup.accountIndex})`));
|
|
539
|
+
const { valid, address } = await validateKey(info.chain, key);
|
|
540
|
+
if (!valid) {
|
|
541
|
+
throw new PerpError("INVALID_PARAMS", `Invalid ${info.chain} private key`, {
|
|
542
|
+
remediation: info.chain === "evm" ? "Expected 32-byte hex (0x-prefixed or not)" : "Expected a Solana secret key (base58, hex, or JSON byte array)",
|
|
543
|
+
});
|
|
677
544
|
}
|
|
545
|
+
// Vault passphrase: --passphrase > OWS_PASSPHRASE > hidden prompt. Piped stdin
|
|
546
|
+
// carries the key, so a non-interactive run must name the passphrase explicitly.
|
|
547
|
+
const flagPp = command.optsWithGlobals().passphrase;
|
|
548
|
+
let passphrase;
|
|
549
|
+
if (flagPp !== undefined)
|
|
550
|
+
passphrase = flagPp;
|
|
551
|
+
else if ("OWS_PASSPHRASE" in process.env)
|
|
552
|
+
passphrase = process.env.OWS_PASSPHRASE ?? "";
|
|
553
|
+
else if (!piped)
|
|
554
|
+
passphrase = await promptHidden("OWS vault passphrase (hidden, empty = none): ");
|
|
678
555
|
else {
|
|
679
|
-
|
|
680
|
-
|
|
556
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "stdin carries the key, so the vault passphrase must come from OWS_PASSPHRASE or --passphrase", {
|
|
557
|
+
remediation: `pbpaste | OWS_PASSPHRASE="$PP" perp wallet set ${exchange} (use OWS_PASSPHRASE= for an unencrypted vault wallet)`,
|
|
558
|
+
});
|
|
681
559
|
}
|
|
682
|
-
|
|
683
|
-
|
|
560
|
+
const name = opts.name ?? resolved;
|
|
561
|
+
const w = await importOwsWallet(name, passphrase, {
|
|
562
|
+
kind: "single",
|
|
563
|
+
key: info.chain === "evm" && !key.startsWith("0x") ? `0x${key}` : key,
|
|
564
|
+
chain: info.chain,
|
|
565
|
+
});
|
|
566
|
+
const settings = loadSettings();
|
|
567
|
+
const previousActiveWallet = settings.owsActiveWallet || null;
|
|
568
|
+
settings.owsActiveWallet = name;
|
|
569
|
+
if (opts.default)
|
|
570
|
+
settings.defaultExchange = resolved;
|
|
571
|
+
saveSettings(settings);
|
|
572
|
+
// No implicit Lighter API-key registration here (it signed a mainnet ChangePubKey on slot 4
|
|
573
|
+
// and echoed the L2 key): trading needs an explicit `perp wallet agent approve lighter`.
|
|
574
|
+
const lighterNextStep = "perp wallet agent approve lighter";
|
|
575
|
+
if (isJson())
|
|
576
|
+
return printJson(jsonOk({
|
|
577
|
+
exchange: resolved, address, owsWallet: w.name, activeWallet: name, previousActiveWallet, default: !!opts.default,
|
|
578
|
+
...(resolved === "lighter" && { lighterApiKey: { registered: false, nextStep: lighterNextStep } }),
|
|
579
|
+
}));
|
|
580
|
+
console.log(chalk.green(`\n ${resolved} key imported into the OWS vault.`));
|
|
581
|
+
console.log(` Address: ${chalk.green(address)}`);
|
|
582
|
+
console.log(` Wallet: ${chalk.white.bold(w.name)} ${chalk.gray("(encrypted in ~/.ows/)")}`);
|
|
583
|
+
console.log(` Active: ${chalk.cyan(name)} (used by all exchanges)${previousActiveWallet && previousActiveWallet !== name ? chalk.yellow(` — was ${previousActiveWallet}; switch back: perp wallet use ${previousActiveWallet}`) : ""}`);
|
|
584
|
+
if (opts.default)
|
|
585
|
+
console.log(` Default: ${chalk.cyan(resolved)}`);
|
|
586
|
+
if (resolved === "lighter") {
|
|
587
|
+
console.log(chalk.yellow(` API Key: not registered — to trade, run: ${lighterNextStep}`));
|
|
588
|
+
}
|
|
589
|
+
console.log();
|
|
590
|
+
});
|
|
684
591
|
});
|
|
685
592
|
// ── show (show configured exchanges with public addresses) ──
|
|
686
593
|
wallet
|
|
@@ -766,13 +673,17 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
766
673
|
if (isJson()) {
|
|
767
674
|
return printJson(jsonOk({
|
|
768
675
|
ows: { wallets: owsWallets, active: activeOwsWallet },
|
|
769
|
-
|
|
676
|
+
// Public fields only — wallets.json entries hold raw private keys (never printed).
|
|
677
|
+
legacy: {
|
|
678
|
+
wallets: Object.fromEntries(Object.entries(store.wallets).map(([k, w]) => [k, { name: w.name, type: w.type, address: w.address, createdAt: w.createdAt }])),
|
|
679
|
+
active: store.active,
|
|
680
|
+
},
|
|
770
681
|
}));
|
|
771
682
|
}
|
|
772
683
|
if (owsWallets.length === 0 && legacyEntries.length === 0) {
|
|
773
684
|
console.log(chalk.gray("\n No wallets found."));
|
|
774
685
|
console.log(chalk.gray(` Create: ${chalk.cyan("perp wallet generate [name]")}`));
|
|
775
|
-
console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import <
|
|
686
|
+
console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import --name <name>")}\n`));
|
|
776
687
|
return;
|
|
777
688
|
}
|
|
778
689
|
if (owsWallets.length > 0) {
|
|
@@ -886,7 +797,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
886
797
|
// ── balance (by wallet name) ──
|
|
887
798
|
wallet
|
|
888
799
|
.command("balance [name]")
|
|
889
|
-
.description("
|
|
800
|
+
.description("On-chain balances for a saved wallet (or the active one): gas token + USDC / USDT on Arbitrum, Ethereum, Base, BNB Chain, Robinhood Chain (USDG) and Solana")
|
|
890
801
|
.option("--testnet", "Use testnet", false)
|
|
891
802
|
.action(async (name, opts) => {
|
|
892
803
|
const store = loadStore();
|
|
@@ -929,8 +840,35 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
929
840
|
const activeEntries = Object.entries(store.active)
|
|
930
841
|
.map(([ex, wn]) => ({ exchange: ex, ...store.wallets[wn] }))
|
|
931
842
|
.filter((e) => e.address);
|
|
932
|
-
//
|
|
843
|
+
// OWS-aware path: vault-based setups have no legacy `active` mapping
|
|
844
|
+
// (it's empty by design — owsActiveWallet replaces per-exchange
|
|
845
|
+
// active wallets). Fetch on-chain balances for both EVM and Solana
|
|
846
|
+
// accounts of the active OWS vault BEFORE falling through to the
|
|
847
|
+
// legacy .env path.
|
|
933
848
|
if (activeEntries.length === 0) {
|
|
849
|
+
const settings = loadSettings();
|
|
850
|
+
if (settings.owsActiveWallet) {
|
|
851
|
+
// Only loading the OWS vault may fall through to the env path; balance reads report
|
|
852
|
+
// their own per-token errors and any other failure propagates.
|
|
853
|
+
let owsAccounts = null;
|
|
854
|
+
try {
|
|
855
|
+
const { loadOws } = await import("../signer/ows-loader.js");
|
|
856
|
+
owsAccounts = loadOws().getWallet(settings.owsActiveWallet).accounts;
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
owsAccounts = null; // OWS module unavailable or vault missing — use the env path below
|
|
860
|
+
}
|
|
861
|
+
if (owsAccounts !== null) {
|
|
862
|
+
const results = [];
|
|
863
|
+
for (const acct of owsAccounts) {
|
|
864
|
+
const vm = acct.chainId.startsWith("solana:") ? "solana" : acct.chainId.startsWith("eip155:") ? "evm" : null;
|
|
865
|
+
if (vm === null)
|
|
866
|
+
continue;
|
|
867
|
+
results.push(await balanceEntry({ wallet: settings.owsActiveWallet }, vm, acct.address, opts.testnet));
|
|
868
|
+
}
|
|
869
|
+
return printBalanceEntries(results, isJson());
|
|
870
|
+
}
|
|
871
|
+
}
|
|
934
872
|
const stored = loadEnvFile();
|
|
935
873
|
const envEntries = [];
|
|
936
874
|
for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
|
|
@@ -945,61 +883,20 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
945
883
|
}
|
|
946
884
|
if (envEntries.length === 0) {
|
|
947
885
|
if (isJson()) {
|
|
948
|
-
printJson({ ok: false, error: { code: "INVALID_PARAMS", message: "No wallets configured. Run 'perp setup' or 'perp wallet set <exchange>
|
|
886
|
+
printJson({ ok: false, error: { code: "INVALID_PARAMS", message: "No wallets configured. Run 'perp setup' or 'perp wallet set <exchange>'." }, meta: { timestamp: new Date().toISOString() } });
|
|
949
887
|
}
|
|
950
888
|
else {
|
|
951
|
-
console.error(chalk.gray("\n No wallets configured. Run 'perp setup' or 'perp wallet set <exchange>
|
|
889
|
+
console.error(chalk.gray("\n No wallets configured. Run 'perp setup' or 'perp wallet set <exchange>'.\n"));
|
|
952
890
|
}
|
|
953
891
|
process.exit(1);
|
|
954
892
|
}
|
|
955
|
-
|
|
956
|
-
if (!isJson())
|
|
957
|
-
console.log(chalk.cyan.bold(`\n ${we.exchange} (${we.address.slice(0, 8)}...)`));
|
|
958
|
-
const balances = we.chain === "solana"
|
|
959
|
-
? await getSolanaBalances(we.address, opts.testnet)
|
|
960
|
-
: await getEvmBalances(we.address, opts.testnet);
|
|
961
|
-
if (isJson()) {
|
|
962
|
-
printJson(jsonOk({ exchange: we.exchange, address: we.address, balances }));
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const rows = balances.map((b) => [
|
|
966
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
967
|
-
]);
|
|
968
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
969
|
-
}
|
|
970
|
-
console.log();
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
for (const ae of activeEntries) {
|
|
974
|
-
if (!isJson())
|
|
975
|
-
console.log(chalk.cyan.bold(`\n ${ae.name} (${ae.exchange})`));
|
|
976
|
-
const balances = ae.type === "solana"
|
|
977
|
-
? await getSolanaBalances(ae.address, opts.testnet)
|
|
978
|
-
: await getEvmBalances(ae.address, opts.testnet);
|
|
979
|
-
if (isJson()) {
|
|
980
|
-
printJson(jsonOk({ wallet: ae.name, exchange: ae.exchange, balances }));
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
const rows = balances.map((b) => [
|
|
984
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
985
|
-
]);
|
|
986
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
893
|
+
return printBalanceEntries(await balanceEntries(envEntries.map(we => ({ label: { exchange: we.exchange }, vm: we.chain, address: we.address })), opts.testnet), isJson());
|
|
987
894
|
}
|
|
988
|
-
|
|
989
|
-
|
|
895
|
+
return printBalanceEntries(await balanceEntries(activeEntries.map(ae => ({
|
|
896
|
+
label: { wallet: ae.name, exchange: ae.exchange }, vm: ae.type === "solana" ? "solana" : "evm", address: ae.address,
|
|
897
|
+
})), opts.testnet), isJson());
|
|
990
898
|
}
|
|
991
|
-
|
|
992
|
-
console.log(chalk.cyan(`\n Fetching balance for "${entry.name}" (${entry.address.slice(0, 8)}...)\n`));
|
|
993
|
-
const balances = entry.type === "solana"
|
|
994
|
-
? await getSolanaBalances(entry.address, opts.testnet)
|
|
995
|
-
: await getEvmBalances(entry.address, opts.testnet);
|
|
996
|
-
if (isJson())
|
|
997
|
-
return printJson(jsonOk({ wallet: entry.name, balances }));
|
|
998
|
-
const rows = balances.map((b) => [
|
|
999
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
1000
|
-
]);
|
|
1001
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
1002
|
-
console.log();
|
|
899
|
+
return printBalanceEntries([await balanceEntry({ wallet: entry.name }, entry.type === "solana" ? "solana" : "evm", entry.address, opts.testnet)], isJson());
|
|
1003
900
|
});
|
|
1004
901
|
// ── direct address balance (kept for convenience) ──
|
|
1005
902
|
wallet
|
|
@@ -1012,8 +909,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1012
909
|
const balances = await getSolanaBalances(address, opts.testnet);
|
|
1013
910
|
if (isJson())
|
|
1014
911
|
return printJson(jsonOk(balances));
|
|
1015
|
-
|
|
1016
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
912
|
+
console.log(balanceTable(balances));
|
|
1017
913
|
console.log(chalk.gray(`\n Network: ${opts.testnet ? "Devnet" : "Mainnet"}\n`));
|
|
1018
914
|
});
|
|
1019
915
|
wallet
|
|
@@ -1023,11 +919,10 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1023
919
|
.action(async (address, opts) => {
|
|
1024
920
|
if (!isJson())
|
|
1025
921
|
console.log(chalk.cyan(`\n Fetching Arbitrum balances for ${address.slice(0, 8)}...${address.slice(-4)}\n`));
|
|
1026
|
-
const balances = await getEvmBalances(address, opts.testnet);
|
|
922
|
+
const balances = await getEvmBalances(address, opts.testnet, [opts.testnet ? "arbitrum-sepolia" : "arbitrum"]);
|
|
1027
923
|
if (isJson())
|
|
1028
924
|
return printJson(jsonOk(balances));
|
|
1029
|
-
|
|
1030
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
925
|
+
console.log(balanceTable(balances));
|
|
1031
926
|
console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
|
|
1032
927
|
});
|
|
1033
928
|
// ── migrate (legacy → OWS) ──
|
|
@@ -1071,7 +966,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1071
966
|
results.push({ name: entry.name, status: "migrated" });
|
|
1072
967
|
}
|
|
1073
968
|
catch (e) {
|
|
1074
|
-
results.push({ name: entry.name, status: "failed", error: e instanceof Error ? e.message : String(e) });
|
|
969
|
+
results.push({ name: entry.name, status: "failed", error: redactSecrets(e instanceof Error ? e.message : String(e), [entry.privateKey]) });
|
|
1075
970
|
}
|
|
1076
971
|
}
|
|
1077
972
|
// Set first migrated wallet as active if none set
|
|
@@ -1269,7 +1164,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1269
1164
|
.requiredOption("--wallets <names>", "Comma-separated OWS wallet names")
|
|
1270
1165
|
.option("--policy <ids>", "Comma-separated policy IDs to enforce")
|
|
1271
1166
|
.option("--expires <iso>", "Key expiry (ISO-8601)")
|
|
1272
|
-
.
|
|
1167
|
+
.addOption(deprecatedPassphraseOption("Vault passphrase", "-p, --passphrase <pass>").default("")).hook("preAction", warnDeprecatedPassphrase)
|
|
1273
1168
|
.action(async (opts, command) => {
|
|
1274
1169
|
try {
|
|
1275
1170
|
const o = loadOws();
|
|
@@ -1391,7 +1286,8 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1391
1286
|
}
|
|
1392
1287
|
// Call OWS CLI subprocess for native MoonPay deposit
|
|
1393
1288
|
// OWS CLI writes addresses to stderr, URL to stdout
|
|
1394
|
-
|
|
1289
|
+
// The OWS CLI child never receives key material (SSOT Rule #3): secret env vars are stripped.
|
|
1290
|
+
const owsEnv = { ...scrubSecretEnv(process.env), PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
|
|
1395
1291
|
try {
|
|
1396
1292
|
const { spawnSync } = await import("child_process");
|
|
1397
1293
|
const proc = spawnSync(owsBin, ["fund", "deposit", "--wallet", walletName, "--chain", chain], {
|
|
@@ -1450,7 +1346,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1450
1346
|
wallet
|
|
1451
1347
|
.command("setup")
|
|
1452
1348
|
.description("One-click setup: create wallet + guardrail policy + agent API key")
|
|
1453
|
-
.option("
|
|
1349
|
+
.option("--name <name>", "Wallet name", "perp-trading")
|
|
1454
1350
|
.option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
|
|
1455
1351
|
.option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
|
|
1456
1352
|
.option("--skip-key", "Skip API key creation")
|
|
@@ -1647,6 +1543,8 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1647
1543
|
// NOTE: Distinct from `wallet key` (OWS API token) and `wallet policy`
|
|
1648
1544
|
// (OWS guardrail). See runOwsCli comment above for the three-way split.
|
|
1649
1545
|
registerWalletAgentCommands(wallet, isJson);
|
|
1546
|
+
// ── Builder fees (off by default; settings builder.<venue>) ──
|
|
1547
|
+
registerWalletBuilderCommands(wallet, isJson, getAdapterForExchange ?? null);
|
|
1650
1548
|
// ── Manage (per-DEX exchange settings — Pacifica + Lighter) ──────────
|
|
1651
1549
|
// Absorbed from former top-level `perp manage`. Holds exchange-specific
|
|
1652
1550
|
// settings: margin mode, subaccount, lake, builder/referral, API keys.
|