perp-cli 0.12.17 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -1,11 +1,17 @@
1
+ import { Option } from "commander";
2
+ import { PerpError } from "../errors.js";
1
3
  import chalk from "chalk";
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
4
+ import bs58 from "bs58";
5
+ import { existsSync, mkdirSync, readFileSync } from "fs";
6
+ import { writeKeystoreAtomic } from "../agent-wallet/venue-keystore.js";
3
7
  import { resolve } from "path";
4
- import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
5
- import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
8
+ import { formatUsd, makeTable, printJson, jsonOk, withJsonErrors } from "../utils.js";
9
+ import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, ENV_FILE } from "./init.js";
10
+ import { scrubSecretEnv } from "../secret-env.js";
6
11
  import { loadSettings, saveSettings } from "../settings.js";
7
- import { registerWalletAgentCommands } from "./agent.js";
12
+ import { deprecatedPassphraseOption, registerWalletAgentCommands, warnDeprecatedPassphrase } from "./agent.js";
8
13
  import { registerWalletManageCommands } from "./manage.js";
14
+ import { registerWalletBuilderCommands } from "./builder.js";
9
15
  import { loadOws } from "../signer/ows-loader.js";
10
16
  // ── OWS CLI subprocess helper (used by backup/restore/rotate) ──
11
17
  //
@@ -41,7 +47,8 @@ async function runOwsCli(args, timeout = 30000) {
41
47
  if (!owsBin) {
42
48
  throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
43
49
  }
44
- const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
50
+ // The OWS CLI child never receives key material (SSOT Rule #3): secret env vars are stripped.
51
+ const owsEnv = { ...scrubSecretEnv(process.env), PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
45
52
  const proc = spawnSync(owsBin, args, { encoding: "utf-8", timeout, env: owsEnv });
46
53
  if (proc.error)
47
54
  throw new Error(`OWS CLI failed: ${proc.error.message}`);
@@ -66,7 +73,8 @@ function loadStore() {
66
73
  }
67
74
  function saveStore(store) {
68
75
  ensurePerpDir();
69
- writeFileSync(WALLETS_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
76
+ // Atomic, explicit 0600 (writeFileSync's mode is ignored when the file already exists).
77
+ writeKeystoreAtomic(WALLETS_FILE, JSON.stringify(store, null, 2));
70
78
  }
71
79
  /** Exported for smart landing page in index.ts */
72
80
  export function getWalletSetupStatus() {
@@ -182,141 +190,114 @@ async function deriveEvmAddress(key) {
182
190
  const pk = key.startsWith("0x") ? key : `0x${key}`;
183
191
  return new ethers.Wallet(pk).address;
184
192
  }
185
- // ── Legacy helpers ───────────────────────────────────────────
186
- async function _legacyGenerate(chain, name, isJson) {
187
- if (chain === "solana") {
188
- const { Keypair } = await import("@solana/web3.js");
189
- const bs58 = (await import("bs58")).default;
190
- const keypair = Keypair.generate();
191
- const address = keypair.publicKey.toBase58();
192
- const privkey = bs58.encode(keypair.secretKey);
193
- const walletName = name || "default-sol";
194
- const store = loadStore();
195
- if (store.wallets[walletName]) {
196
- console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
197
- process.exit(1);
198
- }
199
- store.wallets[walletName] = { name: walletName, type: "solana", address, privateKey: privkey, createdAt: new Date().toISOString() };
200
- if (!store.active.pacifica)
201
- store.active.pacifica = walletName;
202
- saveStore(store);
203
- setEnvVar("PACIFICA_PRIVATE_KEY", privkey);
204
- if (isJson())
205
- return printJson(jsonOk({ name: walletName, type: "solana", address, legacy: true }));
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`));
193
+ /** `--legacy` (raw keys written to ~/.perp/.env + wallets.json) was removed — SSOT Rule #3. */
194
+ function legacyWriteRemoved(cmd) {
195
+ return new PerpError("INVALID_PARAMS", `wallet ${cmd} --legacy was removed: it wrote raw private keys to ~/.perp/.env and wallets.json`, {
196
+ remediation: cmd === "generate"
197
+ ? "Run: perp wallet generate [name] (OWS encrypted vault). Existing ~/.perp/.env keys keep working; move them with perp wallet set <exchange>"
198
+ : "Run: perp wallet import --name <name> [--chain evm|solana|dual] (key via hidden prompt or stdin) or perp wallet set <exchange>",
199
+ });
200
+ }
201
+ /**
202
+ * Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
203
+ * exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
204
+ * base58/JSON-array/hex and normalize to 32-byte hex secret.
205
+ */
206
+ export function normalizeSolanaKey(input) {
207
+ const s = input.trim();
208
+ // Already hex (with optional 0x prefix)
209
+ const hexNoPrefix = s.startsWith("0x") ? s.slice(2) : s;
210
+ if (/^[0-9a-fA-F]{64}$/.test(hexNoPrefix))
211
+ return hexNoPrefix;
212
+ if (/^[0-9a-fA-F]{128}$/.test(hexNoPrefix)) {
213
+ return hexNoPrefix.slice(0, 64); // 64-byte keypair → first 32 bytes secret
211
214
  }
212
- else if (chain === "evm") {
213
- const { ethers } = await import("ethers");
214
- const w = ethers.Wallet.createRandom();
215
- const walletName = name || "default-evm";
216
- const store = loadStore();
217
- if (store.wallets[walletName]) {
218
- console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
219
- process.exit(1);
215
+ // JSON byte array
216
+ if (s.startsWith("[")) {
217
+ let arr;
218
+ try {
219
+ arr = JSON.parse(s);
220
220
  }
221
- store.wallets[walletName] = { name: walletName, type: "evm", address: w.address, privateKey: w.privateKey, createdAt: new Date().toISOString() };
222
- if (!store.active.hyperliquid)
223
- store.active.hyperliquid = walletName;
224
- if (!store.active.lighter)
225
- store.active.lighter = walletName;
226
- saveStore(store);
227
- setEnvVar("HL_PRIVATE_KEY", w.privateKey);
228
- setEnvVar("LIGHTER_PRIVATE_KEY", w.privateKey);
229
- if (isJson())
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`));
221
+ catch {
222
+ // Never surface the parser message: V8 echoes the input (the secret) in it.
223
+ throw new Error("Invalid Solana JSON byte array (not valid JSON)");
224
+ }
225
+ if (arr.length === 64)
226
+ return Buffer.from(arr.slice(0, 32)).toString("hex");
227
+ if (arr.length === 32)
228
+ return Buffer.from(arr).toString("hex");
229
+ throw new Error(`Invalid Solana JSON byte array length: ${arr.length} (expected 32 or 64)`);
230
+ }
231
+ // base58 (Phantom export — typically 87-88 chars for 64-byte keypair)
232
+ try {
233
+ const decoded = bs58.decode(s);
234
+ if (decoded.length === 64)
235
+ return Buffer.from(decoded.slice(0, 32)).toString("hex");
236
+ if (decoded.length === 32)
237
+ return Buffer.from(decoded).toString("hex");
238
+ throw new Error(`Invalid Solana base58 byte length: ${decoded.length} (expected 32 or 64)`);
236
239
  }
237
- else {
238
- console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
239
- process.exit(1);
240
+ catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
240
243
  }
241
244
  }
242
- async function _legacyImport(chain, privateKey, name, isJson) {
243
- if (chain === "solana") {
244
- const { Keypair } = await import("@solana/web3.js");
245
- const bs58 = (await import("bs58")).default;
246
- let address, normalizedKey;
247
- try {
248
- try {
249
- const bytes = bs58.decode(privateKey);
250
- const kp = Keypair.fromSecretKey(bytes);
251
- address = kp.publicKey.toBase58();
252
- normalizedKey = bs58.encode(kp.secretKey);
253
- }
254
- catch {
255
- const arr = JSON.parse(privateKey);
256
- const kp = Keypair.fromSecretKey(Uint8Array.from(arr));
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`);
245
+ /**
246
+ * Import key material into the OWS vault as wallet `name` — the single import
247
+ * path shared by `wallet import` and `perp setup`. Never logs key material.
248
+ */
249
+ /**
250
+ * Strip secret material from an error message before it is shown/logged: the exact secret
251
+ * strings plus any long hex / base58 / byte-array run (keys never reach output — Rule #3).
252
+ */
253
+ export function redactSecrets(message, secrets) {
254
+ let out = message;
255
+ for (const sec of secrets) {
256
+ const t = sec.trim();
257
+ for (const v of [t, t.replace(/^0x/i, "")])
258
+ if (v.length >= 8)
259
+ out = out.split(v).join("[redacted]");
281
260
  }
282
- else if (chain === "evm") {
283
- const { ethers } = await import("ethers");
284
- const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
285
- let address;
286
- try {
287
- address = new ethers.Wallet(pk).address;
288
- }
289
- catch {
290
- console.error(chalk.red("\n Invalid EVM private key.\n"));
291
- process.exit(1);
292
- return;
293
- }
294
- const store = loadStore();
295
- if (store.wallets[name]) {
296
- console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
297
- process.exit(1);
298
- }
299
- store.wallets[name] = { name, type: "evm", address, privateKey: pk, createdAt: new Date().toISOString() };
300
- if (!store.active.hyperliquid)
301
- store.active.hyperliquid = name;
302
- if (!store.active.lighter)
303
- store.active.lighter = name;
304
- saveStore(store);
305
- setEnvVar("HL_PRIVATE_KEY", pk);
306
- setEnvVar("LIGHTER_PRIVATE_KEY", pk);
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`);
261
+ return out
262
+ .replace(/(0x)?[0-9a-fA-F]{32,}/g, "[redacted]")
263
+ .replace(/[1-9A-HJ-NP-Za-km-z]{40,}/g, "[redacted]")
264
+ .replace(/\[\s*\d{1,3}(\s*,\s*\d{1,3}){7,}[^\]]*\]?/g, "[redacted]");
265
+ }
266
+ /** Read all of a piped (non-TTY) stdin. */
267
+ function readAllStdin() {
268
+ return new Promise((resolveRead, reject) => {
269
+ const chunks = [];
270
+ process.stdin.setEncoding("utf-8");
271
+ process.stdin.on("data", (c) => chunks.push(typeof c === "string" ? c : c.toString("utf-8")));
272
+ process.stdin.on("end", () => resolveRead(chunks.join("")));
273
+ process.stdin.on("error", reject);
274
+ });
275
+ }
276
+ export async function importOwsWallet(name, passphrase, input) {
277
+ const secrets = input.kind === "mnemonic" ? [input.mnemonic] : input.kind === "dual" ? [input.evm, input.solana] : [input.key];
278
+ try {
279
+ return importOwsWalletRaw(name, passphrase, input);
312
280
  }
313
- else {
314
- console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
315
- process.exit(1);
281
+ catch (err) {
282
+ const msg = err instanceof Error ? err.message : String(err);
283
+ throw new PerpError("INVALID_PARAMS", `Import failed: ${redactSecrets(msg, secrets)}`, {
284
+ remediation: "Check the key / mnemonic format and retry (the input is never echoed)",
285
+ });
286
+ }
287
+ }
288
+ function importOwsWalletRaw(name, passphrase, input) {
289
+ const ows = loadOws();
290
+ if (input.kind === "mnemonic")
291
+ return ows.importWalletMnemonic(name, input.mnemonic, passphrase);
292
+ if (input.kind === "dual") {
293
+ // Dual-curve: both explicit keys (OWS 1.3+ API — both must be present)
294
+ return ows.importWalletPrivateKey(name, "", // ignored when both secp256k1Key and ed25519Key are provided
295
+ passphrase, undefined, undefined, input.evm, normalizeSolanaKey(input.solana));
316
296
  }
297
+ const key = input.chain === "solana" ? normalizeSolanaKey(input.key) : input.key;
298
+ return ows.importWalletPrivateKey(name, key, passphrase, undefined, input.chain);
317
299
  }
318
- // ── Commands ──────────────────────────────────────────────────
319
- export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter) {
300
+ export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter, getAdapterForExchange) {
320
301
  const wallet = program.command("wallet").description("Wallet management & on-chain balances");
321
302
  // ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
322
303
  wallet
@@ -324,12 +305,12 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
324
305
  .description("Generate a new wallet (OWS encrypted vault, multi-chain)")
325
306
  .option("--words <count>", "Mnemonic word count (12 or 24)", "12")
326
307
  .option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
327
- .option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
328
- .option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
308
+ .addOption(deprecatedPassphraseOption()).hook("preAction", warnDeprecatedPassphrase)
309
+ .addOption(new Option("--legacy <chain>", "Removed — refused (raw keys are never written to ~/.perp/.env)").hideHelp())
329
310
  .action(async (name, opts, command) => {
330
- // Legacy mode for backward compat
331
- if (opts.legacy) {
332
- return _legacyGenerate(opts.legacy, name, isJson);
311
+ if (opts.legacy !== undefined) {
312
+ await withJsonErrors(isJson(), async () => { throw legacyWriteRemoved("generate"); });
313
+ return;
333
314
  }
334
315
  const walletName = name || "default";
335
316
  try {
@@ -399,128 +380,73 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
399
380
  // Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
400
381
  // exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
401
382
  // base58/JSON-array/hex and normalize to 32-byte hex secret.
383
+ // SSOT Rule #3: keys are never accepted on argv (shell history / process list) —
384
+ // hidden TTY prompt or stdin pipe only, matching `wallet set`.
402
385
  wallet
403
386
  .command("import [privateKey]")
404
- .description("Import a private key into OWS encrypted vault (use --evm/--solana to import both curves at once)")
387
+ .description("Import a private key or mnemonic into the OWS encrypted vault (key via hidden prompt or stdin — never argv)")
405
388
  .option("--name <name>", "Wallet alias name", "imported")
406
- .option("--chain <chain>", "Key type for positional <privateKey>: evm (default) or solana", "evm")
407
- .option("--evm <hex>", "EVM (secp256k1) private key — use with --solana to import both curves")
408
- .option("--solana <key>", "Solana (ed25519) private key — accepts hex (64), base58 (Phantom export), or JSON byte array. Use with --evm to import both curves")
409
- .option("--mnemonic", "Import as mnemonic phrase instead of private key")
410
- .option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
411
- .option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
412
- .action(async (privateKey, opts, command) => {
413
- // Legacy mode (positional only)
414
- if (opts.legacy) {
415
- if (!privateKey) {
416
- const msg = "--legacy mode requires a positional <privateKey>";
417
- if (isJson()) {
418
- const { jsonError } = await import("../utils.js");
419
- return printJson(jsonError("OWS_ERROR", msg));
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));
389
+ .option("--chain <chain>", "Key type: evm (default), solana, or dual (EVM + Solana keys: two prompts, or two stdin lines EVM first)", "evm")
390
+ .option("--mnemonic", "Import a mnemonic phrase instead of a private key")
391
+ .addOption(deprecatedPassphraseOption()).hook("preAction", warnDeprecatedPassphrase)
392
+ .addOption(new Option("--evm <hex>", "Removed — refused (key on argv); use --chain evm|dual").hideHelp())
393
+ .addOption(new Option("--solana <key>", "Removed — refused (key on argv); use --chain solana|dual").hideHelp())
394
+ .addOption(new Option("--legacy <chain>", "Removed — refused (raw keys are never written to ~/.perp/.env)").hideHelp())
395
+ .action(async (argvKey, opts, command) => {
396
+ await withJsonErrors(isJson(), async () => {
397
+ if (opts.legacy !== undefined)
398
+ throw legacyWriteRemoved("import");
399
+ if (argvKey !== undefined || opts.evm !== undefined || opts.solana !== undefined) {
400
+ throw new PerpError("INVALID_PARAMS", "Refusing a private key / mnemonic on the command line: it is visible in shell history and the process list", {
401
+ 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.",
402
+ });
443
403
  }
444
- console.error(chalk.red(`\n Error: ${msg}\n`));
445
- process.exit(1);
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));
404
+ const chain = opts.chain;
405
+ if (chain !== "evm" && chain !== "solana" && chain !== "dual") {
406
+ throw new PerpError("INVALID_PARAMS", `Unknown --chain "${chain}"`, { remediation: "Use --chain evm, solana, or dual" });
452
407
  }
453
- console.error(chalk.red(`\n Error: ${msg}\n`));
454
- process.exit(1);
455
- }
456
- try {
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);
408
+ if (opts.mnemonic && chain !== "evm") {
409
+ throw new PerpError("INVALID_PARAMS", "--mnemonic derives every chain itself and cannot be combined with --chain solana|dual", {
410
+ remediation: "Drop --chain: perp wallet import --mnemonic --name <name>",
411
+ });
506
412
  }
507
- else if (opts.evm && opts.solana) {
508
- // Dual-curve: both explicit keys (OWS 1.3+ API — both must be present)
509
- w = ows.importWalletPrivateKey(opts.name, "", // ignored when both secp256k1Key and ed25519Key are provided
510
- pp, undefined, undefined, opts.evm, normalizeSolana(opts.solana));
413
+ const { promptHidden } = await import("./agent.js");
414
+ const piped = process.stdin.isTTY !== true;
415
+ const labels = opts.mnemonic ? ["mnemonic phrase"] : chain === "dual" ? ["EVM private key", "Solana private key"] : [`${chain === "evm" ? "EVM" : "Solana"} private key`];
416
+ let secrets;
417
+ if (piped) {
418
+ const lines = (await readAllStdin()).split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
419
+ secrets = opts.mnemonic ? [lines.join(" ")] : lines;
511
420
  }
512
- else if (opts.evm) {
513
- // Single EVM via flag — same as positional + --chain evm
514
- w = ows.importWalletPrivateKey(opts.name, opts.evm, pp, undefined, "evm");
421
+ else {
422
+ secrets = [];
423
+ for (const label of labels)
424
+ secrets.push((await promptHidden(`${label} (hidden): `)).trim());
515
425
  }
516
- else if (opts.solana) {
517
- // Single Solana via flag — same as positional + --chain solana
518
- w = ows.importWalletPrivateKey(opts.name, normalizeSolana(opts.solana), pp, undefined, "solana");
426
+ if (secrets.length !== labels.length || secrets.some(x => x.length === 0)) {
427
+ 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}`, {
428
+ remediation: "Enter it at the hidden prompt, or pipe it: pbpaste | OWS_PASSPHRASE=... perp wallet import --name <name>",
429
+ });
519
430
  }
431
+ // Vault passphrase: --passphrase > OWS_PASSPHRASE > hidden prompt. Piped stdin
432
+ // carries the key, so a non-interactive run must name the passphrase explicitly.
433
+ // optsWithGlobals(): the root program's --passphrase shadows the subcommand's.
434
+ const flagPp = command.optsWithGlobals().passphrase;
435
+ let pp;
436
+ if (flagPp !== undefined)
437
+ pp = flagPp;
438
+ else if ("OWS_PASSPHRASE" in process.env)
439
+ pp = process.env.OWS_PASSPHRASE ?? "";
440
+ else if (!piped)
441
+ pp = await promptHidden("OWS vault passphrase (hidden, empty = none): ");
520
442
  else {
521
- const keyArg = opts.chain === "solana" ? normalizeSolana(privateKey) : privateKey;
522
- w = ows.importWalletPrivateKey(opts.name, keyArg, pp, undefined, opts.chain);
443
+ throw new PerpError("PASSPHRASE_REQUIRED", "stdin carries the key, so the vault passphrase must come from OWS_PASSPHRASE or --passphrase", {
444
+ remediation: `pbpaste | OWS_PASSPHRASE="$PP" perp wallet import --name ${opts.name} (use OWS_PASSPHRASE= for an unencrypted vault wallet)`,
445
+ });
523
446
  }
447
+ const w = await importOwsWallet(opts.name, pp, opts.mnemonic ? { kind: "mnemonic", mnemonic: secrets[0] }
448
+ : chain === "dual" ? { kind: "dual", evm: secrets[0], solana: secrets[1] }
449
+ : { kind: "single", key: secrets[0], chain });
524
450
  // Set as active wallet if none set
525
451
  const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
526
452
  const settings = ls();
@@ -545,16 +471,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
545
471
  console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
546
472
  }
547
473
  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
- }
474
+ });
558
475
  });
559
476
  // ── use (set active wallet — OWS first, legacy fallback) ──
560
477
  wallet
@@ -590,7 +507,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
590
507
  if (!entry) {
591
508
  console.error(chalk.red(`\n Wallet "${name}" not found in OWS vault or legacy store.\n`));
592
509
  console.error(chalk.gray(` Create: perp wallet generate ${name}`));
593
- console.error(chalk.gray(` Import: perp wallet import <key> -n ${name}\n`));
510
+ console.error(chalk.gray(` Import: perp wallet import --name ${name} (hidden prompt / stdin)\n`));
594
511
  process.exit(1);
595
512
  }
596
513
  const exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
@@ -604,83 +521,92 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
604
521
  console.log(chalk.gray(` Address: ${entry.address}`));
605
522
  console.log(chalk.yellow(` Note: consider migrating to OWS: perp wallet migrate\n`));
606
523
  });
607
- // ── set (configure exchange key → ~/.perp/.env) ──
524
+ // ── set (import an exchange key into the OWS vault → active wallet) ──
525
+ //
526
+ // SSOT Rule #3: the key goes into the encrypted OWS vault, never ~/.perp/.env,
527
+ // and never via argv (shell history / process list). Input: hidden TTY prompt,
528
+ // or stdin pipe. Keys already in a legacy ~/.perp/.env keep working (read path unchanged).
608
529
  wallet
609
- .command("set <exchange> <key>")
610
- .description("Set private key for an exchange")
530
+ .command("set <exchange> [key]")
531
+ .description("Import an exchange private key into the OWS vault and make it the active wallet (key via hidden prompt or stdin — never argv)")
532
+ .option("--name <name>", "OWS wallet name (default: the exchange id, e.g. hyperliquid)")
611
533
  .option("--default", "Also set as default exchange")
612
- .action(async (exchange, key, opts) => {
613
- // Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter, ast → aster)
614
- const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
615
- const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
616
- const info = EXCHANGE_ENV_MAP[resolved];
617
- if (!info) {
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`));
534
+ .action(async (exchange, argvKey, opts, command) => {
535
+ await withJsonErrors(isJson(), async () => {
536
+ if (argvKey !== undefined) {
537
+ throw new PerpError("INVALID_PARAMS", "Refusing a private key on the command line: it is visible in shell history and the process list", {
538
+ 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.`,
539
+ });
631
540
  }
632
- console.error(chalk.red(`\n Invalid ${info.chain} private key.\n`));
633
- process.exit(1);
634
- }
635
- const normalized = info.chain === "evm"
636
- ? (key.startsWith("0x") ? key : `0x${key}`)
637
- : key;
638
- setEnvVar(info.envKey, normalized);
639
- if (opts.default) {
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 };
541
+ // Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter, ast → aster)
542
+ const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
543
+ const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
544
+ const info = EXCHANGE_ENV_MAP[resolved];
545
+ if (!info) {
546
+ throw new PerpError("INVALID_PARAMS", `Unknown exchange: ${exchange}`, {
547
+ 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.",
548
+ });
659
549
  }
660
- catch (e) {
661
- lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
550
+ const { promptHidden } = await import("./agent.js");
551
+ const piped = process.stdin.isTTY !== true;
552
+ const key = (piped ? await readAllStdin() : await promptHidden(`${resolved} private key (hidden): `)).trim();
553
+ if (!key) {
554
+ throw new PerpError("INVALID_PARAMS", "No private key received", {
555
+ remediation: `Enter it at the hidden prompt, or pipe it: pbpaste | perp wallet set ${exchange}`,
556
+ });
662
557
  }
663
- }
664
- if (isJson())
665
- return printJson(jsonOk({
666
- exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default,
667
- ...(resolved === "lighter" && { lighterApiKey: lighterApiSetup }),
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})`));
558
+ const { valid, address } = await validateKey(info.chain, key);
559
+ if (!valid) {
560
+ throw new PerpError("INVALID_PARAMS", `Invalid ${info.chain} private key`, {
561
+ remediation: info.chain === "evm" ? "Expected 32-byte hex (0x-prefixed or not)" : "Expected a Solana secret key (base58, hex, or JSON byte array)",
562
+ });
677
563
  }
564
+ // Vault passphrase: --passphrase > OWS_PASSPHRASE > hidden prompt. Piped stdin
565
+ // carries the key, so a non-interactive run must name the passphrase explicitly.
566
+ const flagPp = command.optsWithGlobals().passphrase;
567
+ let passphrase;
568
+ if (flagPp !== undefined)
569
+ passphrase = flagPp;
570
+ else if ("OWS_PASSPHRASE" in process.env)
571
+ passphrase = process.env.OWS_PASSPHRASE ?? "";
572
+ else if (!piped)
573
+ passphrase = await promptHidden("OWS vault passphrase (hidden, empty = none): ");
678
574
  else {
679
- console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
680
- console.log(chalk.gray(` You can retry: perp wallet agent approve lighter`));
575
+ throw new PerpError("PASSPHRASE_REQUIRED", "stdin carries the key, so the vault passphrase must come from OWS_PASSPHRASE or --passphrase", {
576
+ remediation: `pbpaste | OWS_PASSPHRASE="$PP" perp wallet set ${exchange} (use OWS_PASSPHRASE= for an unencrypted vault wallet)`,
577
+ });
681
578
  }
682
- }
683
- console.log();
579
+ const name = opts.name ?? resolved;
580
+ const w = await importOwsWallet(name, passphrase, {
581
+ kind: "single",
582
+ key: info.chain === "evm" && !key.startsWith("0x") ? `0x${key}` : key,
583
+ chain: info.chain,
584
+ });
585
+ const settings = loadSettings();
586
+ const previousActiveWallet = settings.owsActiveWallet || null;
587
+ settings.owsActiveWallet = name;
588
+ if (opts.default)
589
+ settings.defaultExchange = resolved;
590
+ saveSettings(settings);
591
+ // No implicit Lighter API-key registration here (it signed a mainnet ChangePubKey on slot 4
592
+ // and echoed the L2 key): trading needs an explicit `perp wallet agent approve lighter`.
593
+ const lighterNextStep = "perp wallet agent approve lighter";
594
+ if (isJson())
595
+ return printJson(jsonOk({
596
+ exchange: resolved, address, owsWallet: w.name, activeWallet: name, previousActiveWallet, default: !!opts.default,
597
+ ...(resolved === "lighter" && { lighterApiKey: { registered: false, nextStep: lighterNextStep } }),
598
+ }));
599
+ console.log(chalk.green(`\n ${resolved} key imported into the OWS vault.`));
600
+ console.log(` Address: ${chalk.green(address)}`);
601
+ console.log(` Wallet: ${chalk.white.bold(w.name)} ${chalk.gray("(encrypted in ~/.ows/)")}`);
602
+ console.log(` Active: ${chalk.cyan(name)} (used by all exchanges)${previousActiveWallet && previousActiveWallet !== name ? chalk.yellow(` — was ${previousActiveWallet}; switch back: perp wallet use ${previousActiveWallet}`) : ""}`);
603
+ if (opts.default)
604
+ console.log(` Default: ${chalk.cyan(resolved)}`);
605
+ if (resolved === "lighter") {
606
+ console.log(chalk.yellow(` API Key: not registered — to trade, run: ${lighterNextStep}`));
607
+ }
608
+ console.log();
609
+ });
684
610
  });
685
611
  // ── show (show configured exchanges with public addresses) ──
686
612
  wallet
@@ -766,13 +692,17 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
766
692
  if (isJson()) {
767
693
  return printJson(jsonOk({
768
694
  ows: { wallets: owsWallets, active: activeOwsWallet },
769
- legacy: { wallets: store.wallets, active: store.active },
695
+ // Public fields only — wallets.json entries hold raw private keys (never printed).
696
+ legacy: {
697
+ wallets: Object.fromEntries(Object.entries(store.wallets).map(([k, w]) => [k, { name: w.name, type: w.type, address: w.address, createdAt: w.createdAt }])),
698
+ active: store.active,
699
+ },
770
700
  }));
771
701
  }
772
702
  if (owsWallets.length === 0 && legacyEntries.length === 0) {
773
703
  console.log(chalk.gray("\n No wallets found."));
774
704
  console.log(chalk.gray(` Create: ${chalk.cyan("perp wallet generate [name]")}`));
775
- console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import <key>")}\n`));
705
+ console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import --name <name>")}\n`));
776
706
  return;
777
707
  }
778
708
  if (owsWallets.length > 0) {
@@ -929,8 +859,54 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
929
859
  const activeEntries = Object.entries(store.active)
930
860
  .map(([ex, wn]) => ({ exchange: ex, ...store.wallets[wn] }))
931
861
  .filter((e) => e.address);
932
- // Fallback to .env-based wallets when no named wallets are active
862
+ // OWS-aware path: vault-based setups have no legacy `active` mapping
863
+ // (it's empty by design — owsActiveWallet replaces per-exchange
864
+ // active wallets). Fetch on-chain balances for both EVM and Solana
865
+ // accounts of the active OWS vault BEFORE falling through to the
866
+ // legacy .env path.
933
867
  if (activeEntries.length === 0) {
868
+ const settings = loadSettings();
869
+ if (settings.owsActiveWallet) {
870
+ try {
871
+ const { loadOws } = await import("../signer/ows-loader.js");
872
+ const ows = loadOws();
873
+ const owsWallet = ows.getWallet(settings.owsActiveWallet);
874
+ const owsResults = [];
875
+ for (const acct of owsWallet.accounts) {
876
+ const isEvm = acct.chainId.startsWith("eip155:");
877
+ const isSol = acct.chainId.startsWith("solana:");
878
+ if (!isEvm && !isSol)
879
+ continue;
880
+ const address = acct.address;
881
+ const balances = isSol
882
+ ? await getSolanaBalances(address, opts.testnet)
883
+ : await getEvmBalances(address, opts.testnet);
884
+ if (isJson()) {
885
+ owsResults.push({
886
+ wallet: settings.owsActiveWallet,
887
+ chain: isSol ? "solana" : "evm",
888
+ address,
889
+ balances,
890
+ });
891
+ }
892
+ else {
893
+ console.log(chalk.cyan.bold(`\n ${settings.owsActiveWallet} (${isSol ? "solana" : "evm"} ${address.slice(0, 8)}...)`));
894
+ const rows = balances.map((b) => [
895
+ chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
896
+ ]);
897
+ console.log(makeTable(["Token", "Balance", "USD Value"], rows));
898
+ }
899
+ }
900
+ if (isJson())
901
+ printJson(jsonOk(owsResults));
902
+ else
903
+ console.log();
904
+ return;
905
+ }
906
+ catch {
907
+ // OWS module unavailable or vault missing — fall through to env path
908
+ }
909
+ }
934
910
  const stored = loadEnvFile();
935
911
  const envEntries = [];
936
912
  for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
@@ -945,10 +921,10 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
945
921
  }
946
922
  if (envEntries.length === 0) {
947
923
  if (isJson()) {
948
- printJson({ ok: false, error: { code: "INVALID_PARAMS", message: "No wallets configured. Run 'perp setup' or 'perp wallet set <exchange> <key>'." }, meta: { timestamp: new Date().toISOString() } });
924
+ 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
925
  }
950
926
  else {
951
- console.error(chalk.gray("\n No wallets configured. Run 'perp setup' or 'perp wallet set <exchange> <key>'.\n"));
927
+ console.error(chalk.gray("\n No wallets configured. Run 'perp setup' or 'perp wallet set <exchange>'.\n"));
952
928
  }
953
929
  process.exit(1);
954
930
  }
@@ -1071,7 +1047,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
1071
1047
  results.push({ name: entry.name, status: "migrated" });
1072
1048
  }
1073
1049
  catch (e) {
1074
- results.push({ name: entry.name, status: "failed", error: e instanceof Error ? e.message : String(e) });
1050
+ results.push({ name: entry.name, status: "failed", error: redactSecrets(e instanceof Error ? e.message : String(e), [entry.privateKey]) });
1075
1051
  }
1076
1052
  }
1077
1053
  // Set first migrated wallet as active if none set
@@ -1269,7 +1245,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
1269
1245
  .requiredOption("--wallets <names>", "Comma-separated OWS wallet names")
1270
1246
  .option("--policy <ids>", "Comma-separated policy IDs to enforce")
1271
1247
  .option("--expires <iso>", "Key expiry (ISO-8601)")
1272
- .option("-p, --passphrase <pass>", "Vault passphrase", "")
1248
+ .addOption(deprecatedPassphraseOption("Vault passphrase", "-p, --passphrase <pass>").default("")).hook("preAction", warnDeprecatedPassphrase)
1273
1249
  .action(async (opts, command) => {
1274
1250
  try {
1275
1251
  const o = loadOws();
@@ -1391,7 +1367,8 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
1391
1367
  }
1392
1368
  // Call OWS CLI subprocess for native MoonPay deposit
1393
1369
  // OWS CLI writes addresses to stderr, URL to stdout
1394
- const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
1370
+ // The OWS CLI child never receives key material (SSOT Rule #3): secret env vars are stripped.
1371
+ const owsEnv = { ...scrubSecretEnv(process.env), PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
1395
1372
  try {
1396
1373
  const { spawnSync } = await import("child_process");
1397
1374
  const proc = spawnSync(owsBin, ["fund", "deposit", "--wallet", walletName, "--chain", chain], {
@@ -1450,7 +1427,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
1450
1427
  wallet
1451
1428
  .command("setup")
1452
1429
  .description("One-click setup: create wallet + guardrail policy + agent API key")
1453
- .option("-n, --name <name>", "Wallet name", "perp-trading")
1430
+ .option("--name <name>", "Wallet name", "perp-trading")
1454
1431
  .option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
1455
1432
  .option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
1456
1433
  .option("--skip-key", "Skip API key creation")
@@ -1647,6 +1624,8 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
1647
1624
  // NOTE: Distinct from `wallet key` (OWS API token) and `wallet policy`
1648
1625
  // (OWS guardrail). See runOwsCli comment above for the three-way split.
1649
1626
  registerWalletAgentCommands(wallet, isJson);
1627
+ // ── Builder fees (off by default; settings builder.<venue>) ──
1628
+ registerWalletBuilderCommands(wallet, isJson, getAdapterForExchange ?? null);
1650
1629
  // ── Manage (per-DEX exchange settings — Pacifica + Lighter) ──────────
1651
1630
  // Absorbed from former top-level `perp manage`. Holds exchange-specific
1652
1631
  // settings: margin mode, subaccount, lake, builder/referral, API keys.