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.
Files changed (363) 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 +303 -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 +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  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 +357 -459
  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 +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  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 +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  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 +1386 -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 +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -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 +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -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 { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
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, setEnvVar, ENV_FILE } from "./init.js";
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
- const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
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
- writeFileSync(WALLETS_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
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
- const { Connection, PublicKey } = await import("@solana/web3.js");
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
- async function getEvmBalances(address, isTestnet) {
137
- const { ethers } = await import("ethers");
138
- const rpcUrl = isTestnet
139
- ? "https://sepolia-rollup.arbitrum.io/rpc"
140
- : "https://arb1.arbitrum.io/rpc";
141
- const provider = new ethers.JsonRpcProvider(rpcUrl);
142
- const balances = [];
143
- const ethBal = await provider.getBalance(address);
144
- const ethBalance = Number(ethers.formatEther(ethBal));
145
- balances.push({ token: "ETH", balance: ethBalance.toFixed(6), usdValue: "" });
146
- const usdcAddr = isTestnet
147
- ? "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
148
- : "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
149
- try {
150
- const usdc = new ethers.Contract(usdcAddr, ["function balanceOf(address) view returns (uint256)"], provider);
151
- const rawBal = await usdc.balanceOf(address);
152
- const usdcBalance = Number(ethers.formatUnits(rawBal, 6));
153
- balances.push({ token: "USDC", balance: usdcBalance.toFixed(2), usdValue: `$${formatUsd(usdcBalance)}` });
154
- }
155
- catch {
156
- balances.push({ token: "USDC", balance: "0.00", usdValue: "$0.00" });
157
- }
158
- try {
159
- const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd");
160
- const json = (await res.json());
161
- balances[0].usdValue = `$${formatUsd(ethBalance * (json.ethereum?.usd ?? 0))}`;
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
- catch {
164
- balances[0].usdValue = "-";
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
- return balances;
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
- // ── 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`));
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
- 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);
196
+ // JSON byte array
197
+ if (s.startsWith("[")) {
198
+ let arr;
199
+ try {
200
+ arr = JSON.parse(s);
220
201
  }
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`));
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
- else {
238
- console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
239
- process.exit(1);
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
- 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`);
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
- 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`);
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
- else {
314
- console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
315
- process.exit(1);
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
- // ── Commands ──────────────────────────────────────────────────
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
- .option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
328
- .option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
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
- // Legacy mode for backward compat
331
- if (opts.legacy) {
332
- return _legacyGenerate(opts.legacy, name, isJson);
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 (use --evm/--solana to import both curves at once)")
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 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));
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
- 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));
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
- 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);
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
- 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));
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 if (opts.evm) {
513
- // Single EVM via flag — same as positional + --chain evm
514
- w = ows.importWalletPrivateKey(opts.name, opts.evm, pp, undefined, "evm");
402
+ else {
403
+ secrets = [];
404
+ for (const label of labels)
405
+ secrets.push((await promptHidden(`${label} (hidden): `)).trim());
515
406
  }
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");
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
- const keyArg = opts.chain === "solana" ? normalizeSolana(privateKey) : privateKey;
522
- w = ows.importWalletPrivateKey(opts.name, keyArg, pp, undefined, opts.chain);
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 <key> -n ${name}\n`));
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 (configure exchange key → ~/.perp/.env) ──
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> <key>")
610
- .description("Set private key for an exchange")
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, 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`));
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
- 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 };
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
- catch (e) {
661
- lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
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
- 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})`));
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
- console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
680
- console.log(chalk.gray(` You can retry: perp wallet agent approve lighter`));
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
- console.log();
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
- legacy: { wallets: store.wallets, active: store.active },
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 <key>")}\n`));
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("Check on-chain balance for a saved wallet (or active wallet)")
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
- // Fallback to .env-based wallets when no named wallets are active
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> <key>'." }, meta: { timestamp: new Date().toISOString() } });
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> <key>'.\n"));
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
- for (const we of envEntries) {
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
- console.log();
989
- return;
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
- if (!isJson())
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
- const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
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
- const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
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
- .option("-p, --passphrase <pass>", "Vault passphrase", "")
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
- const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
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("-n, --name <name>", "Wallet name", "perp-trading")
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.