perp-cli 0.9.8 → 0.12.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 (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -4,6 +4,49 @@ import { resolve } from "path";
4
4
  import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
5
5
  import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
6
6
  import { loadSettings, saveSettings } from "../settings.js";
7
+ import { registerWalletAgentCommands } from "./agent.js";
8
+ import { registerWalletManageCommands } from "./manage.js";
9
+ import { loadOws } from "../signer/ows-loader.js";
10
+ // ── OWS CLI subprocess helper (used by backup/restore/rotate) ──
11
+ //
12
+ // The wallet command tree exposes three different "agent" sub-namespaces;
13
+ // they are intentionally separate because they answer three different
14
+ // questions:
15
+ //
16
+ // * `wallet agent approve <ex> ...` — DEX-side agent registration. Sends
17
+ // EIP-712 approveAgent (or equivalent) to the exchange so the agent
18
+ // wallet can post orders on behalf of the master. State lives partly on
19
+ // the DEX (their auth list) and partly in `~/.perp/settings.json` under
20
+ // `agents.<exchange>.<name>`.
21
+ //
22
+ // * `wallet key create / list / revoke` — OWS-side API key. A vault
23
+ // access token (`ows_key_…`) that lets an off-host agent process invoke
24
+ // OWS signing primitives without holding the master passphrase. Pure
25
+ // OWS construct, no DEX traffic.
26
+ //
27
+ // * `wallet policy create / list / show / delete` — OWS-side guardrail.
28
+ // A policy attached to an OWS API key that bounds what the agent can
29
+ // sign (chain allowlist, per-tx USD cap, daily withdraw cap, etc.).
30
+ // Also pure OWS, evaluated locally before any signature is produced.
31
+ //
32
+ // Future maintainers: do NOT collapse these. They protect different layers.
33
+ async function runOwsCli(args, timeout = 30000) {
34
+ const { resolve: pathResolve } = await import("path");
35
+ const { existsSync: fsExists } = await import("fs");
36
+ const { spawnSync } = await import("child_process");
37
+ const owsBin = [
38
+ pathResolve(process.env.HOME || "~", ".ows", "bin", "ows"),
39
+ "/usr/local/bin/ows",
40
+ ].find(p => fsExists(p));
41
+ if (!owsBin) {
42
+ throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
43
+ }
44
+ const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
45
+ const proc = spawnSync(owsBin, args, { encoding: "utf-8", timeout, env: owsEnv });
46
+ if (proc.error)
47
+ throw new Error(`OWS CLI failed: ${proc.error.message}`);
48
+ return { stdout: (proc.stdout || "").trim(), stderr: (proc.stderr || "").trim() };
49
+ }
7
50
  const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
8
51
  const WALLETS_FILE = resolve(PERP_DIR, "wallets.json");
9
52
  function ensurePerpDir() {
@@ -139,101 +182,69 @@ async function deriveEvmAddress(key) {
139
182
  const pk = key.startsWith("0x") ? key : `0x${key}`;
140
183
  return new ethers.Wallet(pk).address;
141
184
  }
142
- // ── Commands ──────────────────────────────────────────────────
143
- export function registerWalletCommands(program, isJson) {
144
- const wallet = program.command("wallet").description("Wallet management & on-chain balances");
145
- // ── generate ──
146
- const generate = wallet.command("generate").description("Generate a new wallet keypair");
147
- generate
148
- .command("solana")
149
- .description("Generate a new Solana keypair")
150
- .option("-n, --name <name>", "Wallet alias name", "default-sol")
151
- .action(async (opts) => {
185
+ // ── Legacy helpers ───────────────────────────────────────────
186
+ async function _legacyGenerate(chain, name, isJson) {
187
+ if (chain === "solana") {
152
188
  const { Keypair } = await import("@solana/web3.js");
153
189
  const bs58 = (await import("bs58")).default;
154
190
  const keypair = Keypair.generate();
155
191
  const address = keypair.publicKey.toBase58();
156
192
  const privkey = bs58.encode(keypair.secretKey);
193
+ const walletName = name || "default-sol";
157
194
  const store = loadStore();
158
- if (store.wallets[opts.name]) {
159
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists. Use a different name or 'wallet remove' first.\n`));
195
+ if (store.wallets[walletName]) {
196
+ console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
160
197
  process.exit(1);
161
198
  }
162
- store.wallets[opts.name] = {
163
- name: opts.name,
164
- type: "solana",
165
- address,
166
- privateKey: privkey,
167
- createdAt: new Date().toISOString(),
168
- };
199
+ store.wallets[walletName] = { name: walletName, type: "solana", address, privateKey: privkey, createdAt: new Date().toISOString() };
169
200
  if (!store.active.pacifica)
170
- store.active.pacifica = opts.name;
201
+ store.active.pacifica = walletName;
171
202
  saveStore(store);
172
- // Also write to .env so loadPrivateKey finds it immediately
173
203
  setEnvVar("PACIFICA_PRIVATE_KEY", privkey);
174
204
  if (isJson())
175
- return printJson(jsonOk({ name: opts.name, type: "solana", address }));
176
- console.log(chalk.cyan.bold("\n New Solana Wallet\n"));
177
- console.log(` Name: ${chalk.white.bold(opts.name)}`);
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)}`);
178
208
  console.log(` Address: ${chalk.green(address)}`);
179
- console.log(` Saved: ${chalk.gray("~/.perp/.env + wallets.json")}`);
180
- if (store.active.pacifica === opts.name) {
181
- console.log(chalk.cyan(`\n Active for: pacifica`));
182
- }
183
- console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
184
- });
185
- generate
186
- .command("evm")
187
- .description("Generate a new EVM wallet")
188
- .option("-n, --name <name>", "Wallet alias name", "default-evm")
189
- .action(async (opts) => {
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`));
211
+ }
212
+ else if (chain === "evm") {
190
213
  const { ethers } = await import("ethers");
191
214
  const w = ethers.Wallet.createRandom();
215
+ const walletName = name || "default-evm";
192
216
  const store = loadStore();
193
- if (store.wallets[opts.name]) {
194
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists. Use a different name or 'wallet remove' first.\n`));
217
+ if (store.wallets[walletName]) {
218
+ console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
195
219
  process.exit(1);
196
220
  }
197
- store.wallets[opts.name] = {
198
- name: opts.name,
199
- type: "evm",
200
- address: w.address,
201
- privateKey: w.privateKey,
202
- createdAt: new Date().toISOString(),
203
- };
221
+ store.wallets[walletName] = { name: walletName, type: "evm", address: w.address, privateKey: w.privateKey, createdAt: new Date().toISOString() };
204
222
  if (!store.active.hyperliquid)
205
- store.active.hyperliquid = opts.name;
223
+ store.active.hyperliquid = walletName;
206
224
  if (!store.active.lighter)
207
- store.active.lighter = opts.name;
225
+ store.active.lighter = walletName;
208
226
  saveStore(store);
209
- // Also write to .env so loadPrivateKey finds it immediately
210
227
  setEnvVar("HL_PRIVATE_KEY", w.privateKey);
211
228
  setEnvVar("LIGHTER_PRIVATE_KEY", w.privateKey);
212
229
  if (isJson())
213
- return printJson(jsonOk({ name: opts.name, type: "evm", address: w.address }));
214
- console.log(chalk.cyan.bold("\n New EVM Wallet\n"));
215
- console.log(` Name: ${chalk.white.bold(opts.name)}`);
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)}`);
216
233
  console.log(` Address: ${chalk.green(w.address)}`);
217
- console.log(` Saved: ${chalk.gray("~/.perp/.env + wallets.json")}`);
218
- const activeFor = Object.entries(store.active)
219
- .filter(([, v]) => v === opts.name)
220
- .map(([k]) => k);
221
- if (activeFor.length)
222
- console.log(chalk.cyan(`\n Active for: ${activeFor.join(", ")}`));
223
- console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
224
- });
225
- // ── import ──
226
- const importCmd = wallet.command("import").description("Import an existing private key");
227
- importCmd
228
- .command("solana <privateKey>")
229
- .description("Import a Solana private key (base58 or JSON array)")
230
- .option("-n, --name <name>", "Wallet alias name", "imported-sol")
231
- .action(async (privateKey, opts) => {
232
- let address;
233
- let normalizedKey = privateKey;
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`));
236
+ }
237
+ else {
238
+ console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
239
+ process.exit(1);
240
+ }
241
+ }
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;
234
247
  try {
235
- const { Keypair } = await import("@solana/web3.js");
236
- const bs58 = (await import("bs58")).default;
237
248
  try {
238
249
  const bytes = bs58.decode(privateKey);
239
250
  const kp = Keypair.fromSecretKey(bytes);
@@ -250,32 +261,25 @@ export function registerWalletCommands(program, isJson) {
250
261
  catch {
251
262
  console.error(chalk.red("\n Invalid Solana private key.\n"));
252
263
  process.exit(1);
264
+ return;
253
265
  }
254
266
  const store = loadStore();
255
- if (store.wallets[opts.name]) {
256
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
267
+ if (store.wallets[name]) {
268
+ console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
257
269
  process.exit(1);
258
270
  }
259
- store.wallets[opts.name] = {
260
- name: opts.name, type: "solana", address, privateKey: normalizedKey,
261
- createdAt: new Date().toISOString(),
262
- };
271
+ store.wallets[name] = { name, type: "solana", address, privateKey: normalizedKey, createdAt: new Date().toISOString() };
263
272
  if (!store.active.pacifica)
264
- store.active.pacifica = opts.name;
273
+ store.active.pacifica = name;
265
274
  saveStore(store);
266
- // Also write to .env so loadPrivateKey finds it immediately
267
275
  setEnvVar("PACIFICA_PRIVATE_KEY", normalizedKey);
268
276
  if (isJson())
269
- return printJson(jsonOk({ name: opts.name, type: "solana", address }));
270
- console.log(chalk.cyan.bold("\n Solana Wallet Imported\n"));
271
- console.log(` Name: ${chalk.white.bold(opts.name)}`);
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)}`);
272
280
  console.log(` Address: ${chalk.green(address)}\n`);
273
- });
274
- importCmd
275
- .command("evm <privateKey>")
276
- .description("Import an EVM private key (0x hex)")
277
- .option("-n, --name <name>", "Wallet alias name", "imported-evm")
278
- .action(async (privateKey, opts) => {
281
+ }
282
+ else if (chain === "evm") {
279
283
  const { ethers } = await import("ethers");
280
284
  const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
281
285
  let address;
@@ -285,70 +289,298 @@ export function registerWalletCommands(program, isJson) {
285
289
  catch {
286
290
  console.error(chalk.red("\n Invalid EVM private key.\n"));
287
291
  process.exit(1);
292
+ return;
288
293
  }
289
294
  const store = loadStore();
290
- if (store.wallets[opts.name]) {
291
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
295
+ if (store.wallets[name]) {
296
+ console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
292
297
  process.exit(1);
293
298
  }
294
- store.wallets[opts.name] = {
295
- name: opts.name, type: "evm", address, privateKey: pk,
296
- createdAt: new Date().toISOString(),
297
- };
299
+ store.wallets[name] = { name, type: "evm", address, privateKey: pk, createdAt: new Date().toISOString() };
298
300
  if (!store.active.hyperliquid)
299
- store.active.hyperliquid = opts.name;
301
+ store.active.hyperliquid = name;
300
302
  if (!store.active.lighter)
301
- store.active.lighter = opts.name;
303
+ store.active.lighter = name;
302
304
  saveStore(store);
303
- // Also write to .env so loadPrivateKey finds it immediately
304
305
  setEnvVar("HL_PRIVATE_KEY", pk);
305
306
  setEnvVar("LIGHTER_PRIVATE_KEY", pk);
306
307
  if (isJson())
307
- return printJson(jsonOk({ name: opts.name, type: "evm", address }));
308
- console.log(chalk.cyan.bold("\n EVM Wallet Imported\n"));
309
- console.log(` Name: ${chalk.white.bold(opts.name)}`);
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)}`);
310
311
  console.log(` Address: ${chalk.green(address)}\n`);
311
- });
312
- // ── use (set active wallet for exchange) ──
312
+ }
313
+ else {
314
+ console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
315
+ process.exit(1);
316
+ }
317
+ }
318
+ // ── Commands ──────────────────────────────────────────────────
319
+ export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter) {
320
+ const wallet = program.command("wallet").description("Wallet management & on-chain balances");
321
+ // ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
313
322
  wallet
314
- .command("use <name> [exchange]")
315
- .description("Set active wallet for exchange (auto-detects if omitted)")
316
- .option("--for <exchange>", "Exchange to bind (legacy alias)")
317
- .action(async (name, exchangeArg, opts) => {
318
- const store = loadStore();
319
- const entry = store.wallets[name];
320
- if (!entry) {
321
- console.error(chalk.red(`\n Wallet "${name}" not found. Run 'perp wallet list' to see available wallets.\n`));
323
+ .command("generate [name]")
324
+ .description("Generate a new wallet (OWS encrypted vault, multi-chain)")
325
+ .option("--words <count>", "Mnemonic word count (12 or 24)", "12")
326
+ .option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
327
+ .option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
328
+ .action(async (name, opts) => {
329
+ // Legacy mode for backward compat
330
+ if (opts.legacy) {
331
+ return _legacyGenerate(opts.legacy, name, isJson);
332
+ }
333
+ const walletName = name || "default";
334
+ try {
335
+ const ows = loadOws();
336
+ const w = ows.createWallet(walletName, "", parseInt(opts.words));
337
+ // Set as active wallet
338
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
339
+ const settings = ls();
340
+ if (!settings.owsActiveWallet) {
341
+ settings.owsActiveWallet = walletName;
342
+ ss(settings);
343
+ }
344
+ if (isJson()) {
345
+ const data = {
346
+ id: w.id, name: w.name, accounts: w.accounts, createdAt: w.createdAt,
347
+ };
348
+ if (opts.showMnemonic)
349
+ data.mnemonic = ows.exportWallet(walletName);
350
+ return printJson(jsonOk(data));
351
+ }
352
+ console.log(chalk.cyan.bold("\n Wallet Created (OWS Encrypted Vault)\n"));
353
+ console.log(` Name: ${chalk.white.bold(w.name)}`);
354
+ console.log(` ID: ${chalk.gray(w.id)}`);
355
+ console.log();
356
+ const evmAcct = w.accounts.find((a) => a.chainId.startsWith("eip155:"));
357
+ const solAcct = w.accounts.find((a) => a.chainId.startsWith("solana:"));
358
+ if (evmAcct)
359
+ console.log(` EVM: ${chalk.green(evmAcct.address)}`);
360
+ if (solAcct)
361
+ console.log(` Solana: ${chalk.green(solAcct.address)}`);
362
+ console.log(chalk.gray(`\n Vault: ~/.ows/ (AES-256-GCM encrypted)`));
363
+ if (settings.owsActiveWallet === walletName) {
364
+ console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
365
+ }
366
+ if (opts.showMnemonic) {
367
+ const mnemonic = ows.exportWallet(walletName);
368
+ console.log(chalk.red.bold(`\n Mnemonic: ${mnemonic}`));
369
+ console.log(chalk.red(" Write this down and store it safely!"));
370
+ }
371
+ console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
372
+ }
373
+ catch (e) {
374
+ const msg = e instanceof Error ? e.message : String(e);
375
+ if (isJson()) {
376
+ const { jsonError } = await import("../utils.js");
377
+ return printJson(jsonError("OWS_ERROR", msg));
378
+ }
379
+ console.error(chalk.red(`\n Error: ${msg}\n`));
322
380
  process.exit(1);
323
381
  }
324
- // Resolve exchange: positional arg > --for flag > auto-detect from wallet type
325
- const rawExchange = exchangeArg || opts.for;
326
- let exchanges;
327
- if (rawExchange) {
328
- const ex = rawExchange.toLowerCase();
329
- const needsSolana = ex === "pacifica";
330
- if (needsSolana && entry.type !== "solana") {
331
- console.error(chalk.red(`\n Pacifica requires a Solana wallet. "${name}" is EVM.\n`));
382
+ });
383
+ // ── import (OWS by default — imports key into encrypted vault) ──
384
+ //
385
+ // Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
386
+ // exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
387
+ // base58/JSON-array/hex and normalize to 32-byte hex secret.
388
+ wallet
389
+ .command("import [privateKey]")
390
+ .description("Import a private key into OWS encrypted vault (use --evm/--solana to import both curves at once)")
391
+ .option("--name <name>", "Wallet alias name", "imported")
392
+ .option("--chain <chain>", "Key type for positional <privateKey>: evm (default) or solana", "evm")
393
+ .option("--evm <hex>", "EVM (secp256k1) private key — use with --solana to import both curves")
394
+ .option("--solana <key>", "Solana (ed25519) private key — accepts hex (64), base58 (Phantom export), or JSON byte array. Use with --evm to import both curves")
395
+ .option("--mnemonic", "Import as mnemonic phrase instead of private key")
396
+ .option("--passphrase <pp>", "OWS vault encryption passphrase (default: empty / OWS_PASSPHRASE env)")
397
+ .option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
398
+ .action(async (privateKey, opts) => {
399
+ // Legacy mode (positional only)
400
+ if (opts.legacy) {
401
+ if (!privateKey) {
402
+ const msg = "--legacy mode requires a positional <privateKey>";
403
+ if (isJson()) {
404
+ const { jsonError } = await import("../utils.js");
405
+ return printJson(jsonError("OWS_ERROR", msg));
406
+ }
407
+ console.error(chalk.red(`\n Error: ${msg}\n`));
332
408
  process.exit(1);
333
409
  }
334
- if (!needsSolana && entry.type !== "evm") {
335
- console.error(chalk.red(`\n ${ex} requires an EVM wallet. "${name}" is Solana.\n`));
336
- process.exit(1);
410
+ return _legacyImport(opts.legacy, privateKey, opts.name, isJson);
411
+ }
412
+ // Validation: input mode is exactly one of (positional | --evm/--solana flags | mnemonic)
413
+ const hasPositional = !!privateKey;
414
+ const hasFlagKeys = !!(opts.evm || opts.solana);
415
+ if (!hasPositional && !hasFlagKeys) {
416
+ const msg = "Provide a <privateKey> argument OR --evm/--solana flag(s)";
417
+ if (isJson()) {
418
+ const { jsonError } = await import("../utils.js");
419
+ return printJson(jsonError("OWS_ERROR", msg));
337
420
  }
338
- exchanges = [ex];
421
+ console.error(chalk.red(`\n Error: ${msg}\n`));
422
+ process.exit(1);
339
423
  }
340
- else {
341
- // Auto-detect from wallet type
342
- exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
424
+ if (hasPositional && hasFlagKeys) {
425
+ const msg = "Cannot mix positional <privateKey> with --evm/--solana flags. Use one input style.";
426
+ if (isJson()) {
427
+ const { jsonError } = await import("../utils.js");
428
+ return printJson(jsonError("OWS_ERROR", msg));
429
+ }
430
+ console.error(chalk.red(`\n Error: ${msg}\n`));
431
+ process.exit(1);
432
+ }
433
+ if (opts.mnemonic && hasFlagKeys) {
434
+ const msg = "--mnemonic cannot be combined with --evm/--solana";
435
+ if (isJson()) {
436
+ const { jsonError } = await import("../utils.js");
437
+ return printJson(jsonError("OWS_ERROR", msg));
438
+ }
439
+ console.error(chalk.red(`\n Error: ${msg}\n`));
440
+ process.exit(1);
441
+ }
442
+ try {
443
+ const ows = loadOws();
444
+ // Auto-normalize Solana keys: accept hex / base58 (Phantom) / JSON byte array
445
+ const bs58 = (await import("bs58")).default;
446
+ const normalizeSolana = (input) => {
447
+ const s = input.trim();
448
+ // Already hex (with optional 0x prefix)
449
+ const hexNoPrefix = s.startsWith("0x") ? s.slice(2) : s;
450
+ if (/^[0-9a-fA-F]{64}$/.test(hexNoPrefix))
451
+ return hexNoPrefix;
452
+ if (/^[0-9a-fA-F]{128}$/.test(hexNoPrefix)) {
453
+ return hexNoPrefix.slice(0, 64); // 64-byte keypair → first 32 bytes secret
454
+ }
455
+ // JSON byte array
456
+ if (s.startsWith("[")) {
457
+ const arr = JSON.parse(s);
458
+ if (arr.length === 64)
459
+ return Buffer.from(arr.slice(0, 32)).toString("hex");
460
+ if (arr.length === 32)
461
+ return Buffer.from(arr).toString("hex");
462
+ throw new Error(`Invalid Solana JSON byte array length: ${arr.length} (expected 32 or 64)`);
463
+ }
464
+ // base58 (Phantom export — typically 87-88 chars for 64-byte keypair)
465
+ try {
466
+ const decoded = bs58.decode(s);
467
+ if (decoded.length === 64)
468
+ return Buffer.from(decoded.slice(0, 32)).toString("hex");
469
+ if (decoded.length === 32)
470
+ return Buffer.from(decoded).toString("hex");
471
+ throw new Error(`Invalid Solana base58 byte length: ${decoded.length} (expected 32 or 64)`);
472
+ }
473
+ catch (err) {
474
+ const msg = err instanceof Error ? err.message : String(err);
475
+ throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
476
+ }
477
+ };
478
+ // Resolve vault passphrase (precedence: --passphrase > OWS_PASSPHRASE env > "")
479
+ const pp = opts.passphrase
480
+ ?? (("OWS_PASSPHRASE" in process.env) ? (process.env["OWS_PASSPHRASE"] ?? "") : "");
481
+ let w;
482
+ if (opts.mnemonic) {
483
+ w = ows.importWalletMnemonic(opts.name, privateKey, pp);
484
+ }
485
+ else if (opts.evm && opts.solana) {
486
+ // Dual-curve: both explicit keys (OWS 1.3+ API — both must be present)
487
+ w = ows.importWalletPrivateKey(opts.name, "", // ignored when both secp256k1Key and ed25519Key are provided
488
+ pp, undefined, undefined, opts.evm, normalizeSolana(opts.solana));
489
+ }
490
+ else if (opts.evm) {
491
+ // Single EVM via flag — same as positional + --chain evm
492
+ w = ows.importWalletPrivateKey(opts.name, opts.evm, pp, undefined, "evm");
493
+ }
494
+ else if (opts.solana) {
495
+ // Single Solana via flag — same as positional + --chain solana
496
+ w = ows.importWalletPrivateKey(opts.name, normalizeSolana(opts.solana), pp, undefined, "solana");
497
+ }
498
+ else {
499
+ const keyArg = opts.chain === "solana" ? normalizeSolana(privateKey) : privateKey;
500
+ w = ows.importWalletPrivateKey(opts.name, keyArg, pp, undefined, opts.chain);
501
+ }
502
+ // Set as active wallet if none set
503
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
504
+ const settings = ls();
505
+ if (!settings.owsActiveWallet) {
506
+ settings.owsActiveWallet = opts.name;
507
+ ss(settings);
508
+ }
509
+ if (isJson())
510
+ return printJson(jsonOk({
511
+ id: w.id, name: w.name, accounts: w.accounts,
512
+ }));
513
+ console.log(chalk.cyan.bold("\n Key Imported to OWS Vault\n"));
514
+ console.log(` Name: ${chalk.white.bold(w.name)}`);
515
+ const evmAcct = w.accounts.find((a) => a.chainId.startsWith("eip155:"));
516
+ const solAcct = w.accounts.find((a) => a.chainId.startsWith("solana:"));
517
+ if (evmAcct)
518
+ console.log(` EVM: ${chalk.green(evmAcct.address)}`);
519
+ if (solAcct)
520
+ console.log(` Solana: ${chalk.green(solAcct.address)}`);
521
+ console.log(chalk.gray(`\n Encrypted in ~/.ows/ vault`));
522
+ if (settings.owsActiveWallet === opts.name) {
523
+ console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
524
+ }
525
+ console.log();
343
526
  }
527
+ catch (e) {
528
+ const msg = e instanceof Error ? e.message : String(e);
529
+ if (isJson()) {
530
+ const { jsonError } = await import("../utils.js");
531
+ return printJson(jsonError("OWS_ERROR", msg));
532
+ }
533
+ console.error(chalk.red(`\n Error: ${msg}\n`));
534
+ process.exit(1);
535
+ }
536
+ });
537
+ // ── use (set active wallet — OWS first, legacy fallback) ──
538
+ wallet
539
+ .command("use <name>")
540
+ .description("Set active wallet (OWS or legacy)")
541
+ .action(async (name) => {
542
+ // Try OWS first
543
+ try {
544
+ const ows = loadOws();
545
+ const w = ows.getWallet(name);
546
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
547
+ const settings = ls();
548
+ settings.owsActiveWallet = name;
549
+ ss(settings);
550
+ const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "-";
551
+ const solAddr = w.accounts.find((a) => a.chainId.startsWith("solana:"))?.address ?? "-";
552
+ if (isJson())
553
+ return printJson(jsonOk({
554
+ wallet: name, type: "ows",
555
+ evm: evmAddr, solana: solAddr,
556
+ activeFor: "all exchanges",
557
+ }));
558
+ console.log(chalk.green(`\n ${chalk.white.bold(name)} is now the active wallet (all exchanges)`));
559
+ console.log(` EVM: ${chalk.green(evmAddr)}`);
560
+ console.log(` Solana: ${chalk.green(solAddr)}\n`);
561
+ return;
562
+ }
563
+ catch {
564
+ // Not an OWS wallet — try legacy
565
+ }
566
+ const store = loadStore();
567
+ const entry = store.wallets[name];
568
+ if (!entry) {
569
+ console.error(chalk.red(`\n Wallet "${name}" not found in OWS vault or legacy store.\n`));
570
+ console.error(chalk.gray(` Create: perp wallet generate ${name}`));
571
+ console.error(chalk.gray(` Import: perp wallet import <key> -n ${name}\n`));
572
+ process.exit(1);
573
+ }
574
+ const exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
344
575
  for (const exchange of exchanges) {
345
576
  store.active[exchange] = name;
346
577
  }
347
578
  saveStore(store);
348
579
  if (isJson())
349
- return printJson(jsonOk({ exchanges, wallet: name, address: entry.address }));
580
+ return printJson(jsonOk({ wallet: name, type: "legacy", exchanges, address: entry.address }));
350
581
  console.log(chalk.green(`\n ${chalk.white.bold(name)} is now active for ${chalk.cyan(exchanges.join(", "))}`));
351
- console.log(chalk.gray(` Address: ${entry.address}\n`));
582
+ console.log(chalk.gray(` Address: ${entry.address}`));
583
+ console.log(chalk.yellow(` Note: consider migrating to OWS: perp wallet migrate\n`));
352
584
  });
353
585
  // ── set (configure exchange key → ~/.perp/.env) ──
354
586
  wallet
@@ -396,24 +628,21 @@ export function registerWalletCommands(program, isJson) {
396
628
  await adapter.init();
397
629
  const apiKeyIndex = 4;
398
630
  const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
399
- setEnvVar("LIGHTER_API_KEY", apiKey);
400
- setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
401
- setEnvVar("LIGHTER_API_KEY_INDEX", String(apiKeyIndex));
631
+ // SSOT Rule #3: persist L2 key in encrypted keystore.
632
+ // accountIndex/apiKeyIndex live in settings.agents.lighter and don't
633
+ // need env duplication.
634
+ const { saveLighterKey } = await import("../agent-wallet/lighter-keystore.js");
635
+ saveLighterKey(adapter.accountIndex, apiKeyIndex, apiKey, "");
402
636
  lighterApiSetup = { apiKey, accountIndex: adapter.accountIndex };
403
637
  }
404
638
  catch (e) {
405
639
  lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
406
640
  }
407
641
  }
408
- const settings = loadSettings();
409
- const referralHint = !settings.referrals
410
- ? { referrals: false, hint: "Optional: 'perp settings referrals on' to support perp-cli development (no extra fees — exchange rebates only)" }
411
- : undefined;
412
642
  if (isJson())
413
643
  return printJson(jsonOk({
414
644
  exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default,
415
645
  ...(resolved === "lighter" && { lighterApiKey: lighterApiSetup }),
416
- ...(referralHint && { referralHint }),
417
646
  }));
418
647
  console.log(chalk.green(`\n ${resolved} configured.`));
419
648
  console.log(` Address: ${chalk.green(address)}`);
@@ -426,13 +655,9 @@ export function registerWalletCommands(program, isJson) {
426
655
  }
427
656
  else {
428
657
  console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
429
- console.log(chalk.gray(` You can retry: perp -e lighter manage setup-api-key`));
658
+ console.log(chalk.gray(` You can retry: perp wallet agent approve lighter`));
430
659
  }
431
660
  }
432
- if (!settings.referrals) {
433
- console.log(chalk.gray(` Optional: ${chalk.cyan("perp settings referrals on")} to support perp-cli development`));
434
- console.log(chalk.gray(` (no extra fees — exchange rebates only)`));
435
- }
436
661
  console.log();
437
662
  });
438
663
  // ── show (show configured exchanges with public addresses) ──
@@ -457,14 +682,41 @@ export function registerWalletCommands(program, isJson) {
457
682
  const { valid, address } = await validateKey(entry.chain, entry.key);
458
683
  results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
459
684
  }
685
+ // SSOT Rule #3: surface OWS active wallet's publicAddresses so agents and
686
+ // commands like `wallet agent verify` can read the master EVM/Solana
687
+ // addresses without relying on env-derived keys.
688
+ const settings = loadSettings();
689
+ const activeOwsName = settings.owsActiveWallet;
690
+ let owsActive = null;
691
+ if (activeOwsName) {
692
+ try {
693
+ const w = loadOws().getWallet(activeOwsName);
694
+ const evm = w.accounts.find(a => a.chainId.startsWith("eip155:"))?.address ?? null;
695
+ const sol = w.accounts.find(a => a.chainId.startsWith("solana:"))?.address ?? null;
696
+ owsActive = { name: activeOwsName, evmAddress: evm, solanaAddress: sol };
697
+ }
698
+ catch {
699
+ // OWS unavailable or wallet missing — leave owsActive null. SSOT Rule #2:
700
+ // no fallback to env-derived addresses for the OWS slot.
701
+ owsActive = { name: activeOwsName, evmAddress: null, solanaAddress: null };
702
+ }
703
+ }
460
704
  if (isJson()) {
461
705
  const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
462
- return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
706
+ return printJson(jsonOk({ envFile: ENV_FILE, owsActive, exchanges: data }));
463
707
  }
464
708
  console.log(chalk.cyan.bold("\n Configured Wallets\n"));
465
- if (results.length === 0) {
709
+ if (owsActive) {
710
+ console.log(` ${chalk.cyan("OWS Active".padEnd(14))} ${chalk.white.bold(owsActive.name)}`);
711
+ if (owsActive.evmAddress)
712
+ console.log(` ${chalk.cyan(" EVM".padEnd(14))} ${chalk.green(owsActive.evmAddress)}`);
713
+ if (owsActive.solanaAddress)
714
+ console.log(` ${chalk.cyan(" Solana".padEnd(14))} ${chalk.green(owsActive.solanaAddress)}`);
715
+ console.log();
716
+ }
717
+ if (results.length === 0 && !owsActive) {
466
718
  console.log(chalk.gray(" No keys configured."));
467
- console.log(chalk.gray(` Run ${chalk.cyan("perp setup")} or ${chalk.cyan("perp wallet set <exchange> <key>")}\n`));
719
+ console.log(chalk.gray(` Run ${chalk.cyan("perp setup")} or ${chalk.cyan("perp wallet generate")}\n`));
468
720
  return;
469
721
  }
470
722
  for (const { name, address, source } of results) {
@@ -472,49 +724,100 @@ export function registerWalletCommands(program, isJson) {
472
724
  }
473
725
  console.log();
474
726
  });
475
- // ── list ──
727
+ // ── list (OWS + legacy) ──
476
728
  wallet
477
729
  .command("list")
478
- .description("List all saved wallets (legacy wallets.json)")
730
+ .description("List all wallets (OWS vault + legacy)")
479
731
  .action(async () => {
732
+ const { loadSettings: ls } = await import("../settings.js");
733
+ const settings = ls();
734
+ const activeOwsWallet = settings.owsActiveWallet;
735
+ // OWS wallets
736
+ let owsWallets = [];
737
+ try {
738
+ owsWallets = loadOws().listWallets();
739
+ }
740
+ catch { /* OWS not available */ }
741
+ // Legacy wallets
480
742
  const store = loadStore();
481
- const entries = Object.values(store.wallets);
482
- if (isJson())
483
- return printJson(jsonOk({ wallets: store.wallets, active: store.active }));
484
- if (entries.length === 0) {
743
+ const legacyEntries = Object.values(store.wallets);
744
+ if (isJson()) {
745
+ return printJson(jsonOk({
746
+ ows: { wallets: owsWallets, active: activeOwsWallet },
747
+ legacy: { wallets: store.wallets, active: store.active },
748
+ }));
749
+ }
750
+ if (owsWallets.length === 0 && legacyEntries.length === 0) {
485
751
  console.log(chalk.gray("\n No wallets found."));
486
- console.log(chalk.gray(" Use 'perp wallet generate' or 'perp wallet import' to add one.\n"));
752
+ console.log(chalk.gray(` Create: ${chalk.cyan("perp wallet generate [name]")}`));
753
+ console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import <key>")}\n`));
487
754
  return;
488
755
  }
489
- // Build reverse map: wallet name -> which exchanges it's active for
490
- const activeMap = new Map();
491
- for (const [exchange, walletName] of Object.entries(store.active)) {
492
- if (!activeMap.has(walletName))
493
- activeMap.set(walletName, []);
494
- activeMap.get(walletName).push(exchange);
495
- }
496
- console.log(chalk.cyan.bold("\n Saved Wallets\n"));
497
- const rows = entries.map((w) => {
498
- const activeFor = activeMap.get(w.name) ?? [];
499
- const activeStr = activeFor.length
500
- ? chalk.cyan(activeFor.join(", "))
501
- : chalk.gray("-");
502
- return [
503
- chalk.white.bold(w.name),
504
- w.type,
505
- chalk.green(w.address.slice(0, 10) + "..." + w.address.slice(-6)),
506
- activeStr,
507
- chalk.gray(w.createdAt.split("T")[0]),
508
- ];
509
- });
510
- console.log(makeTable(["Name", "Type", "Address", "Active For", "Created"], rows));
756
+ if (owsWallets.length > 0) {
757
+ console.log(chalk.cyan.bold("\n OWS Vault Wallets") + chalk.gray(" (encrypted, multi-chain)\n"));
758
+ const rows = owsWallets.map(w => {
759
+ const evmAddr = w.accounts.find(a => a.chainId.startsWith("eip155:"))?.address ?? "-";
760
+ const solAddr = w.accounts.find(a => a.chainId.startsWith("solana:"))?.address ?? "-";
761
+ const isActive = w.name === activeOwsWallet;
762
+ return [
763
+ (isActive ? chalk.cyan("*") + " " : " ") + chalk.white.bold(w.name),
764
+ chalk.green(evmAddr.slice(0, 10) + "..." + evmAddr.slice(-4)),
765
+ chalk.green(solAddr.slice(0, 6) + "..." + solAddr.slice(-4)),
766
+ chalk.gray(w.createdAt.split("T")[0]),
767
+ ];
768
+ });
769
+ console.log(makeTable(["Name", "EVM Address", "Solana Address", "Created"], rows));
770
+ if (activeOwsWallet) {
771
+ console.log(chalk.gray(` * = active wallet`));
772
+ }
773
+ }
774
+ if (legacyEntries.length > 0) {
775
+ const activeMap = new Map();
776
+ for (const [exchange, walletName] of Object.entries(store.active)) {
777
+ if (!activeMap.has(walletName))
778
+ activeMap.set(walletName, []);
779
+ activeMap.get(walletName).push(exchange);
780
+ }
781
+ console.log(chalk.yellow.bold("\n Legacy Wallets") + chalk.gray(" (unencrypted, ~/.perp/wallets.json)\n"));
782
+ const rows = legacyEntries.map(w => {
783
+ const activeFor = activeMap.get(w.name) ?? [];
784
+ const activeStr = activeFor.length ? chalk.cyan(activeFor.join(", ")) : chalk.gray("-");
785
+ return [
786
+ chalk.white.bold(w.name),
787
+ w.type,
788
+ chalk.green(w.address.slice(0, 10) + "..." + w.address.slice(-6)),
789
+ activeStr,
790
+ chalk.gray(w.createdAt.split("T")[0]),
791
+ ];
792
+ });
793
+ console.log(makeTable(["Name", "Type", "Address", "Active For", "Created"], rows));
794
+ console.log(chalk.yellow(` Tip: migrate to OWS with ${chalk.cyan("perp wallet migrate")}`));
795
+ }
511
796
  console.log();
512
797
  });
513
- // ── remove ──
798
+ // ── remove (OWS + legacy) ──
514
799
  wallet
515
800
  .command("remove <name>")
516
- .description("Remove a saved wallet")
801
+ .description("Remove a wallet (OWS or legacy)")
517
802
  .action(async (name) => {
803
+ // Try OWS first
804
+ try {
805
+ const ows = loadOws();
806
+ const w = ows.getWallet(name);
807
+ ows.deleteWallet(name);
808
+ // Clear active if this was the active wallet
809
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
810
+ const settings = ls();
811
+ if (settings.owsActiveWallet === name) {
812
+ settings.owsActiveWallet = "";
813
+ ss(settings);
814
+ }
815
+ if (isJson())
816
+ return printJson(jsonOk({ removed: name, id: w.id, type: "ows" }));
817
+ console.log(chalk.yellow(`\n OWS wallet "${name}" removed from vault.\n`));
818
+ return;
819
+ }
820
+ catch { /* not in OWS — try legacy */ }
518
821
  const store = loadStore();
519
822
  if (!store.wallets[name]) {
520
823
  console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
@@ -522,15 +825,14 @@ export function registerWalletCommands(program, isJson) {
522
825
  }
523
826
  const address = store.wallets[name].address;
524
827
  delete store.wallets[name];
525
- // Clear active references
526
828
  for (const [exchange, walletName] of Object.entries(store.active)) {
527
829
  if (walletName === name)
528
830
  delete store.active[exchange];
529
831
  }
530
832
  saveStore(store);
531
833
  if (isJson())
532
- return printJson(jsonOk({ removed: name, address }));
533
- console.log(chalk.yellow(`\n Wallet "${name}" removed.`));
834
+ return printJson(jsonOk({ removed: name, address, type: "legacy" }));
835
+ console.log(chalk.yellow(`\n Legacy wallet "${name}" removed.`));
534
836
  console.log(chalk.gray(` Address was: ${address}\n`));
535
837
  });
536
838
  // ── rename ──
@@ -570,7 +872,33 @@ export function registerWalletCommands(program, isJson) {
570
872
  if (name) {
571
873
  entry = store.wallets[name];
572
874
  if (!entry) {
573
- console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
875
+ // Try OWS vault before failing — balance only needs address + chain type
876
+ try {
877
+ const { loadOws } = await import("../signer/ows-loader.js");
878
+ const ows = loadOws();
879
+ const owsWallet = ows.getWallet(name);
880
+ const evmAccount = owsWallet.accounts.find((a) => a.chainId.startsWith("eip155:"));
881
+ const solAccount = owsWallet.accounts.find((a) => a.chainId.startsWith("solana:"));
882
+ const account = evmAccount ?? solAccount;
883
+ if (account) {
884
+ entry = {
885
+ name,
886
+ type: evmAccount ? "evm" : "solana",
887
+ address: account.address,
888
+ privateKey: "", // OWS wallet — PK stays encrypted in vault
889
+ createdAt: new Date().toISOString(),
890
+ };
891
+ }
892
+ }
893
+ catch { /* not in OWS either */ }
894
+ }
895
+ if (!entry) {
896
+ if (isJson()) {
897
+ printJson({ ok: false, error: { code: "KEY_NOT_FOUND", message: `Wallet "${name}" not found in OWS vault or legacy store.` }, meta: { timestamp: new Date().toISOString() } });
898
+ }
899
+ else {
900
+ console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
901
+ }
574
902
  process.exit(1);
575
903
  }
576
904
  }
@@ -680,4 +1008,622 @@ export function registerWalletCommands(program, isJson) {
680
1008
  console.log(makeTable(["Token", "Balance", "USD Value"], rows));
681
1009
  console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
682
1010
  });
1011
+ // ── migrate (legacy → OWS) ──
1012
+ wallet
1013
+ .command("migrate")
1014
+ .description("Migrate legacy wallets (wallets.json) to OWS encrypted vault")
1015
+ .action(async () => {
1016
+ const store = loadStore();
1017
+ const entries = Object.values(store.wallets);
1018
+ if (entries.length === 0) {
1019
+ if (isJson())
1020
+ return printJson(jsonOk({ migrated: 0 }));
1021
+ console.log(chalk.gray("\n No legacy wallets to migrate.\n"));
1022
+ return;
1023
+ }
1024
+ const ows = loadOws();
1025
+ const results = [];
1026
+ for (const entry of entries) {
1027
+ try {
1028
+ try {
1029
+ ows.getWallet(entry.name);
1030
+ results.push({ name: entry.name, status: "skipped (already in OWS)" });
1031
+ continue;
1032
+ }
1033
+ catch { /* good — doesn't exist */ }
1034
+ const chain = entry.type === "solana" ? "solana" : "evm";
1035
+ let pk = entry.privateKey;
1036
+ // OWS expects hex private keys — convert bs58 Solana keys
1037
+ if (chain === "solana" && !pk.startsWith("0x")) {
1038
+ try {
1039
+ const bs58 = (await import("bs58")).default;
1040
+ const bytes = bs58.decode(pk);
1041
+ const keyBytes = bytes.length === 64 ? bytes.slice(0, 32) : bytes;
1042
+ pk = "0x" + Buffer.from(keyBytes).toString("hex");
1043
+ }
1044
+ catch {
1045
+ // If it's already hex or a JSON array, try as-is
1046
+ }
1047
+ }
1048
+ ows.importWalletPrivateKey(entry.name, pk, "", undefined, chain);
1049
+ results.push({ name: entry.name, status: "migrated" });
1050
+ }
1051
+ catch (e) {
1052
+ results.push({ name: entry.name, status: "failed", error: e instanceof Error ? e.message : String(e) });
1053
+ }
1054
+ }
1055
+ // Set first migrated wallet as active if none set
1056
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
1057
+ const settings = ls();
1058
+ if (!settings.owsActiveWallet) {
1059
+ const firstMigrated = results.find(r => r.status === "migrated");
1060
+ if (firstMigrated) {
1061
+ settings.owsActiveWallet = firstMigrated.name;
1062
+ ss(settings);
1063
+ }
1064
+ }
1065
+ if (isJson())
1066
+ return printJson(jsonOk({ results }));
1067
+ console.log(chalk.cyan.bold("\n Migration Results\n"));
1068
+ for (const r of results) {
1069
+ const icon = r.status === "migrated" ? chalk.green("OK") : r.status.startsWith("skipped") ? chalk.yellow("SKIP") : chalk.red("FAIL");
1070
+ console.log(` ${icon} ${chalk.white.bold(r.name)} — ${r.status}${r.error ? ` (${r.error})` : ""}`);
1071
+ }
1072
+ const migrated = results.filter(r => r.status === "migrated").length;
1073
+ if (migrated > 0) {
1074
+ console.log(chalk.green(`\n ${migrated} wallet(s) migrated to OWS vault.`));
1075
+ console.log(chalk.gray(" Keys are now encrypted at ~/.ows/"));
1076
+ console.log(chalk.yellow(" You can remove legacy wallets.json after verifying.\n"));
1077
+ }
1078
+ else {
1079
+ console.log();
1080
+ }
1081
+ });
1082
+ // ── Policy Engine (OWS) ──────────────────────────────────────────────
1083
+ // OWS-side guardrail: rules an agent API key must satisfy at sign time.
1084
+ const policyCmd = wallet.command("policy").description("Policy engine — guardrails for agent signing");
1085
+ policyCmd
1086
+ .command("create")
1087
+ .description("Create a signing policy")
1088
+ .requiredOption("--id <id>", "Unique policy ID")
1089
+ .requiredOption("--name <name>", "Human-readable policy name")
1090
+ .option("--chains <chains>", "Comma-separated allowed CAIP-2 chain IDs")
1091
+ .option("--expires <iso>", "Policy expiry (ISO-8601 timestamp)")
1092
+ .option("--executable <path>", "Path to custom policy executable")
1093
+ .option("--perp-defaults", "Auto-configure with perp-cli guardrail (DEX contracts + spending limits)")
1094
+ .option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
1095
+ .option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
1096
+ .option("--max-withdraw-usd <amount>", "Max per-withdrawal USD", "500")
1097
+ .option("--max-daily-withdraw-usd <amount>", "Max daily withdrawal USD", "2000")
1098
+ .option("--block-approve", "Block approve() and transferFrom() calls")
1099
+ .action(async (opts) => {
1100
+ try {
1101
+ const o = loadOws();
1102
+ const rules = [];
1103
+ if (opts.perpDefaults) {
1104
+ const { ALLOWED_CHAINS } = await import("../guardrail/contracts.js");
1105
+ rules.push({ type: "allowed_chains", chain_ids: ALLOWED_CHAINS });
1106
+ }
1107
+ else if (opts.chains) {
1108
+ rules.push({ type: "allowed_chains", chain_ids: opts.chains.split(",").map(s => s.trim()) });
1109
+ }
1110
+ if (opts.expires) {
1111
+ rules.push({ type: "expires_at", timestamp: opts.expires });
1112
+ }
1113
+ let executablePath = opts.executable;
1114
+ let config;
1115
+ if (opts.perpDefaults) {
1116
+ const { resolve: pathResolve } = await import("path");
1117
+ try {
1118
+ const { createRequire: cr } = await import("node:module");
1119
+ const req = cr(import.meta.url);
1120
+ executablePath = pathResolve(req.resolve("perp-cli/dist/guardrail/perp-guardrail.js"));
1121
+ }
1122
+ catch {
1123
+ executablePath = pathResolve(process.cwd(), "node_modules/.bin/perp-guardrail");
1124
+ }
1125
+ config = {
1126
+ max_tx_usd: parseInt(opts.maxTxUsd || "1000"),
1127
+ max_daily_usd: parseInt(opts.maxDailyUsd || "5000"),
1128
+ max_withdraw_usd: parseInt(opts.maxWithdrawUsd || "500"),
1129
+ max_daily_withdraw_usd: parseInt(opts.maxDailyWithdrawUsd || "2000"),
1130
+ ...(opts.blockApprove ? { block_selectors: ["0x095ea7b3", "0x23b872dd"] } : {}),
1131
+ };
1132
+ }
1133
+ const policyDef = {
1134
+ id: opts.id, name: opts.name, version: 1, created_at: new Date().toISOString(),
1135
+ rules, executable: executablePath || null, config: config || null, action: "deny",
1136
+ };
1137
+ o.createPolicy(JSON.stringify(policyDef));
1138
+ if (isJson())
1139
+ return printJson(jsonOk(policyDef));
1140
+ console.log(chalk.green(`\n Policy "${opts.name}" created (${opts.id})`));
1141
+ if (opts.chains || opts.perpDefaults) {
1142
+ const chainRule = rules.find(r => r.type === "allowed_chains");
1143
+ if (chainRule)
1144
+ console.log(` Chains: ${chalk.cyan(chainRule.chain_ids.length + " chains")}`);
1145
+ }
1146
+ if (opts.expires)
1147
+ console.log(` Expires: ${chalk.yellow(opts.expires)}`);
1148
+ if (executablePath)
1149
+ console.log(` Executable: ${chalk.gray(executablePath)}`);
1150
+ if (config)
1151
+ console.log(` Limits: ${chalk.cyan(`$${config.max_tx_usd}/tx, $${config.max_daily_usd}/day`)}`);
1152
+ console.log();
1153
+ }
1154
+ catch (e) {
1155
+ const msg = e instanceof Error ? e.message : String(e);
1156
+ if (isJson()) {
1157
+ const { jsonError } = await import("../utils.js");
1158
+ return printJson(jsonError("OWS_ERROR", msg));
1159
+ }
1160
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1161
+ process.exit(1);
1162
+ }
1163
+ });
1164
+ policyCmd
1165
+ .command("list")
1166
+ .description("List all policies")
1167
+ .action(async () => {
1168
+ try {
1169
+ const o = loadOws();
1170
+ const policies = o.listPolicies();
1171
+ if (isJson())
1172
+ return printJson(jsonOk({ policies }));
1173
+ if (policies.length === 0) {
1174
+ console.log(chalk.gray("\n No policies defined.\n"));
1175
+ return;
1176
+ }
1177
+ console.log(chalk.cyan.bold("\n OWS Policies\n"));
1178
+ const rows = policies.map(p => {
1179
+ const ruleTypes = p.rules.map(r => r.type).join(", ");
1180
+ return [chalk.white.bold(p.id), p.name, chalk.cyan(ruleTypes), chalk.gray(p.created_at?.split("T")[0] ?? "-")];
1181
+ });
1182
+ console.log(makeTable(["ID", "Name", "Rules", "Created"], rows));
1183
+ console.log();
1184
+ }
1185
+ catch (e) {
1186
+ const msg = e instanceof Error ? e.message : String(e);
1187
+ if (isJson()) {
1188
+ const { jsonError } = await import("../utils.js");
1189
+ return printJson(jsonError("OWS_ERROR", msg));
1190
+ }
1191
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1192
+ process.exit(1);
1193
+ }
1194
+ });
1195
+ policyCmd
1196
+ .command("show <id>")
1197
+ .description("Show policy details")
1198
+ .action(async (id) => {
1199
+ try {
1200
+ const o = loadOws();
1201
+ const p = o.getPolicy(id);
1202
+ if (isJson())
1203
+ return printJson(jsonOk(p));
1204
+ console.log(chalk.cyan.bold(`\n Policy: ${id}\n`));
1205
+ console.log(JSON.stringify(p, null, 2));
1206
+ console.log();
1207
+ }
1208
+ catch (e) {
1209
+ const msg = e instanceof Error ? e.message : String(e);
1210
+ if (isJson()) {
1211
+ const { jsonError } = await import("../utils.js");
1212
+ return printJson(jsonError("OWS_ERROR", msg));
1213
+ }
1214
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1215
+ process.exit(1);
1216
+ }
1217
+ });
1218
+ policyCmd
1219
+ .command("delete <id>")
1220
+ .description("Delete a policy")
1221
+ .action(async (id) => {
1222
+ try {
1223
+ const o = loadOws();
1224
+ o.deletePolicy(id);
1225
+ if (isJson())
1226
+ return printJson(jsonOk({ deleted: id }));
1227
+ console.log(chalk.yellow(`\n Policy "${id}" deleted.\n`));
1228
+ }
1229
+ catch (e) {
1230
+ const msg = e instanceof Error ? e.message : String(e);
1231
+ if (isJson()) {
1232
+ const { jsonError } = await import("../utils.js");
1233
+ return printJson(jsonError("OWS_ERROR", msg));
1234
+ }
1235
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1236
+ process.exit(1);
1237
+ }
1238
+ });
1239
+ // ── API Keys (OWS) ──────────────────────────────────────────────────
1240
+ // OWS-side scoped agent access tokens. NOT to be confused with
1241
+ // `wallet agent` (DEX-side delegation) — see comment near runOwsCli.
1242
+ const keyCmd = wallet.command("key").description("API key management — scoped agent access tokens");
1243
+ keyCmd
1244
+ .command("create")
1245
+ .description("Create an API key for policy-gated agent signing")
1246
+ .requiredOption("--name <name>", "Key name (e.g. trading-bot)")
1247
+ .requiredOption("--wallets <names>", "Comma-separated OWS wallet names")
1248
+ .option("--policy <ids>", "Comma-separated policy IDs to enforce")
1249
+ .option("--expires <iso>", "Key expiry (ISO-8601)")
1250
+ .option("-p, --passphrase <pass>", "Vault passphrase", "")
1251
+ .action(async (opts) => {
1252
+ try {
1253
+ const o = loadOws();
1254
+ const walletIds = opts.wallets.split(",").map(s => s.trim());
1255
+ const policyIds = opts.policy ? opts.policy.split(",").map(s => s.trim()) : [];
1256
+ const result = o.createApiKey(opts.name, walletIds, policyIds, opts.passphrase, opts.expires);
1257
+ if (isJson())
1258
+ return printJson(jsonOk({ id: result.id, name: result.name, token: result.token, wallets: walletIds, policies: policyIds, expires: opts.expires ?? null }));
1259
+ console.log(chalk.green.bold("\n OWS API Key Created\n"));
1260
+ console.log(` Name: ${chalk.white.bold(result.name)}`);
1261
+ console.log(` ID: ${chalk.gray(result.id)}`);
1262
+ console.log(` Wallets: ${chalk.cyan(walletIds.join(", "))}`);
1263
+ if (policyIds.length)
1264
+ console.log(` Policies: ${chalk.cyan(policyIds.join(", "))}`);
1265
+ if (opts.expires)
1266
+ console.log(` Expires: ${chalk.yellow(opts.expires)}`);
1267
+ console.log();
1268
+ console.log(` Token: ${chalk.yellow.bold(result.token)}`);
1269
+ console.log(chalk.red("\n Save this token — it will not be shown again!\n"));
1270
+ console.log(chalk.gray(" Usage: perp --ows <wallet> --ows-key <token> -e hl trade ..."));
1271
+ console.log(chalk.gray(" or: OWS_API_KEY=<token> perp --ows <wallet> -e hl trade ...\n"));
1272
+ }
1273
+ catch (e) {
1274
+ const msg = e instanceof Error ? e.message : String(e);
1275
+ if (isJson()) {
1276
+ const { jsonError } = await import("../utils.js");
1277
+ return printJson(jsonError("OWS_ERROR", msg));
1278
+ }
1279
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1280
+ process.exit(1);
1281
+ }
1282
+ });
1283
+ keyCmd
1284
+ .command("list")
1285
+ .description("List all API keys (tokens are never displayed)")
1286
+ .action(async () => {
1287
+ try {
1288
+ const o = loadOws();
1289
+ const keys = o.listApiKeys();
1290
+ if (isJson())
1291
+ return printJson(jsonOk({ keys }));
1292
+ if (keys.length === 0) {
1293
+ console.log(chalk.gray("\n No API keys found.\n"));
1294
+ return;
1295
+ }
1296
+ console.log(chalk.cyan.bold("\n OWS API Keys\n"));
1297
+ const rows = keys.map(k => [
1298
+ chalk.white.bold(k.name), chalk.gray(k.id), chalk.gray(k.created_at?.split("T")[0] ?? "-"),
1299
+ ]);
1300
+ console.log(makeTable(["Name", "ID", "Created"], rows));
1301
+ console.log();
1302
+ }
1303
+ catch (e) {
1304
+ const msg = e instanceof Error ? e.message : String(e);
1305
+ if (isJson()) {
1306
+ const { jsonError } = await import("../utils.js");
1307
+ return printJson(jsonError("OWS_ERROR", msg));
1308
+ }
1309
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1310
+ process.exit(1);
1311
+ }
1312
+ });
1313
+ keyCmd
1314
+ .command("revoke <id>")
1315
+ .description("Revoke an API key (token becomes permanently invalid)")
1316
+ .action(async (id) => {
1317
+ try {
1318
+ const o = loadOws();
1319
+ o.revokeApiKey(id);
1320
+ if (isJson())
1321
+ return printJson(jsonOk({ revoked: id }));
1322
+ console.log(chalk.yellow(`\n API key "${id}" revoked.\n`));
1323
+ }
1324
+ catch (e) {
1325
+ const msg = e instanceof Error ? e.message : String(e);
1326
+ if (isJson()) {
1327
+ const { jsonError } = await import("../utils.js");
1328
+ return printJson(jsonError("OWS_ERROR", msg));
1329
+ }
1330
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1331
+ process.exit(1);
1332
+ }
1333
+ });
1334
+ // ── deposit (OWS-backed MoonPay on-ramp) ─────────────────────────────
1335
+ wallet
1336
+ .command("deposit <walletName>")
1337
+ .description("Create multi-chain deposit via MoonPay (auto-converts to USDC)")
1338
+ .option("-c, --chain <chain>", "Target chain for USDC delivery")
1339
+ .option("--for <exchange>", "Target exchange (auto-selects chain: hl→arbitrum, pac→solana, lt→ethereum)")
1340
+ .action(async (walletName, opts) => {
1341
+ try {
1342
+ // Resolve chain from exchange if specified
1343
+ const exchangeChainMap = {
1344
+ hyperliquid: "arbitrum", hl: "arbitrum",
1345
+ pacifica: "solana", pac: "solana",
1346
+ lighter: "ethereum", lt: "ethereum",
1347
+ };
1348
+ const forExchange = opts.for;
1349
+ const chain = opts.chain || (forExchange ? exchangeChainMap[forExchange.toLowerCase()] : undefined) || "base";
1350
+ // Verify wallet exists first
1351
+ const o = loadOws();
1352
+ o.getWallet(walletName);
1353
+ // Find OWS CLI binary
1354
+ const { resolve: pathResolve } = await import("path");
1355
+ const { existsSync: fsExists } = await import("fs");
1356
+ const owsBin = [
1357
+ pathResolve(process.env.HOME || "~", ".ows", "bin", "ows"),
1358
+ "/usr/local/bin/ows",
1359
+ ].find(p => fsExists(p));
1360
+ if (!owsBin) {
1361
+ throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
1362
+ }
1363
+ // Call OWS CLI subprocess for native MoonPay deposit
1364
+ // OWS CLI writes addresses to stderr, URL to stdout
1365
+ const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
1366
+ try {
1367
+ const { spawnSync } = await import("child_process");
1368
+ const proc = spawnSync(owsBin, ["fund", "deposit", "--wallet", walletName, "--chain", chain], {
1369
+ encoding: "utf-8", timeout: 30000, env: owsEnv,
1370
+ });
1371
+ const stderrOutput = proc.stderr || "";
1372
+ const combined = stderrOutput + "\n" + (proc.stdout || "");
1373
+ // Parse deposit addresses
1374
+ const addresses = {};
1375
+ for (const line of combined.split("\n")) {
1376
+ const match = line.match(/^\s+(bitcoin|ethereum|solana|tron)\s+(\S+)/);
1377
+ if (match)
1378
+ addresses[match[1]] = match[2];
1379
+ }
1380
+ const urlMatch = combined.match(/(https:\/\/moonpay\S+)/);
1381
+ const depositUrl = urlMatch?.[1] ?? null;
1382
+ // Find our wallet's target address
1383
+ const w = o.getWallet(walletName);
1384
+ const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "";
1385
+ if (isJson()) {
1386
+ return printJson(jsonOk({
1387
+ wallet: walletName,
1388
+ chain,
1389
+ exchange: forExchange || null,
1390
+ targetAddress: evmAddr,
1391
+ depositAddresses: addresses,
1392
+ depositUrl,
1393
+ note: `Send any crypto to the deposit addresses. Funds auto-convert to USDC and deliver to ${evmAddr} on ${chain}.`,
1394
+ }));
1395
+ }
1396
+ // Pass through the human-readable output
1397
+ if (stderrOutput.trim()) {
1398
+ console.log();
1399
+ console.log(stderrOutput.trim());
1400
+ }
1401
+ if (depositUrl)
1402
+ console.log(`\n${depositUrl}`);
1403
+ console.log();
1404
+ return;
1405
+ }
1406
+ catch (spawnErr) {
1407
+ throw new Error(`OWS CLI deposit failed: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`);
1408
+ }
1409
+ }
1410
+ catch (e) {
1411
+ const msg = e instanceof Error ? e.message : String(e);
1412
+ if (isJson()) {
1413
+ const { jsonError } = await import("../utils.js");
1414
+ return printJson(jsonError("OWS_ERROR", msg));
1415
+ }
1416
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1417
+ process.exit(1);
1418
+ }
1419
+ });
1420
+ // ── Setup Wizard (one-click vault + policy + agent key) ──────────────
1421
+ wallet
1422
+ .command("setup")
1423
+ .description("One-click setup: create wallet + guardrail policy + agent API key")
1424
+ .option("-n, --name <name>", "Wallet name", "perp-trading")
1425
+ .option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
1426
+ .option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
1427
+ .option("--skip-key", "Skip API key creation")
1428
+ .action(async (opts) => {
1429
+ try {
1430
+ const o = loadOws();
1431
+ // Step 1: Create or reuse wallet
1432
+ let w;
1433
+ let walletCreated = false;
1434
+ try {
1435
+ w = o.getWallet(opts.name);
1436
+ if (!isJson())
1437
+ console.log(chalk.gray(`\n Wallet "${opts.name}" already exists. Using it.`));
1438
+ }
1439
+ catch {
1440
+ w = o.createWallet(opts.name, "", 12);
1441
+ walletCreated = true;
1442
+ const settings = loadSettings();
1443
+ if (!settings.owsActiveWallet) {
1444
+ settings.owsActiveWallet = opts.name;
1445
+ saveSettings(settings);
1446
+ }
1447
+ }
1448
+ const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "-";
1449
+ const solAddr = w.accounts.find((a) => a.chainId.startsWith("solana:"))?.address ?? "-";
1450
+ if (!isJson() && walletCreated) {
1451
+ console.log(chalk.green.bold(`\n 1. Wallet Created: ${opts.name}`));
1452
+ console.log(` EVM: ${chalk.green(evmAddr)}`);
1453
+ console.log(` Solana: ${chalk.green(solAddr)}`);
1454
+ }
1455
+ // Step 2: Create guardrail policy
1456
+ const policyId = `perp-guardrail-${opts.name}`;
1457
+ const { ALLOWED_CHAINS } = await import("../guardrail/contracts.js");
1458
+ const { resolve: pathResolve } = await import("path");
1459
+ let executablePath;
1460
+ try {
1461
+ const { createRequire: cr } = await import("node:module");
1462
+ const req = cr(import.meta.url);
1463
+ executablePath = pathResolve(req.resolve("perp-cli/dist/guardrail/perp-guardrail.js"));
1464
+ }
1465
+ catch {
1466
+ executablePath = pathResolve(process.cwd(), "node_modules/.bin/perp-guardrail");
1467
+ }
1468
+ const policyDef = {
1469
+ id: policyId, name: `perp-cli guardrail (${opts.name})`, version: 1,
1470
+ created_at: new Date().toISOString(),
1471
+ rules: [{ type: "allowed_chains", chain_ids: ALLOWED_CHAINS }],
1472
+ executable: executablePath,
1473
+ config: {
1474
+ max_tx_usd: parseInt(opts.maxTxUsd),
1475
+ max_daily_usd: parseInt(opts.maxDailyUsd),
1476
+ max_withdraw_usd: Math.round(parseInt(opts.maxTxUsd) / 2),
1477
+ max_daily_withdraw_usd: Math.round(parseInt(opts.maxDailyUsd) / 2.5),
1478
+ block_selectors: ["0x095ea7b3", "0x23b872dd"],
1479
+ },
1480
+ action: "deny",
1481
+ };
1482
+ try {
1483
+ o.deletePolicy(policyId);
1484
+ }
1485
+ catch { /* doesn't exist yet */ }
1486
+ o.createPolicy(JSON.stringify(policyDef));
1487
+ if (!isJson()) {
1488
+ console.log(chalk.green.bold(`\n 2. Guardrail Policy Created: ${policyId}`));
1489
+ console.log(` Limits: ${chalk.cyan(`$${opts.maxTxUsd}/tx, $${opts.maxDailyUsd}/day`)}`);
1490
+ console.log(` Chains: ${chalk.cyan(`${ALLOWED_CHAINS.length} chains (all perp-cli exchanges)`)}`);
1491
+ }
1492
+ // Step 3: Create API key
1493
+ let token;
1494
+ let keyId;
1495
+ if (!opts.skipKey) {
1496
+ const keyName = `${opts.name}-agent`;
1497
+ const result = o.createApiKey(keyName, [opts.name], [policyId], "", undefined);
1498
+ token = result.token;
1499
+ keyId = result.id;
1500
+ if (!isJson()) {
1501
+ console.log(chalk.green.bold(`\n 3. Agent API Key Created: ${keyName}`));
1502
+ console.log(` ID: ${chalk.gray(keyId)}`);
1503
+ }
1504
+ }
1505
+ if (isJson()) {
1506
+ return printJson(jsonOk({
1507
+ wallet: { name: opts.name, evm: evmAddr, solana: solAddr, created: walletCreated },
1508
+ policy: { id: policyId, maxTxUsd: parseInt(opts.maxTxUsd), maxDailyUsd: parseInt(opts.maxDailyUsd) },
1509
+ apiKey: token ? { id: keyId, token } : null,
1510
+ }));
1511
+ }
1512
+ console.log(chalk.cyan.bold("\n ─── Setup Complete ───\n"));
1513
+ if (token) {
1514
+ console.log(` ${chalk.yellow.bold("Agent Token:")} ${chalk.yellow(token)}`);
1515
+ console.log(chalk.red(" Save this token — it will not be shown again!\n"));
1516
+ console.log(chalk.white.bold(" Usage:\n"));
1517
+ console.log(chalk.gray(" # Owner mode (no policy limits):"));
1518
+ console.log(` ${chalk.cyan(`perp --ows ${opts.name} -e hl trade buy ETH 0.1`)}\n`);
1519
+ console.log(chalk.gray(" # Agent mode (policy enforced):"));
1520
+ console.log(` ${chalk.cyan(`perp --ows ${opts.name} --ows-key ${token.slice(0, 16)}... -e hl trade buy ETH 0.1`)}\n`);
1521
+ }
1522
+ console.log(chalk.gray(" Fund your wallet before trading:"));
1523
+ console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for hl`)} ${chalk.gray("(Hyperliquid → Arbitrum USDC)")}`);
1524
+ console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for pac`)} ${chalk.gray("(Pacifica → Solana USDC)")}`);
1525
+ console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for lt`)} ${chalk.gray("(Lighter → Ethereum USDC)")}\n`);
1526
+ }
1527
+ catch (e) {
1528
+ const msg = e instanceof Error ? e.message : String(e);
1529
+ if (isJson()) {
1530
+ const { jsonError } = await import("../utils.js");
1531
+ return printJson(jsonError("OWS_ERROR", msg));
1532
+ }
1533
+ console.error(chalk.red(`\n OWS setup error: ${msg}\n`));
1534
+ process.exit(1);
1535
+ }
1536
+ });
1537
+ // ── Backup / Restore (encrypted vault archive) ────────────────────────
1538
+ wallet
1539
+ .command("backup")
1540
+ .description("Backup entire OWS vault (encrypted archive)")
1541
+ .option("-o, --output <path>", "Output file path", `ows-backup-${new Date().toISOString().split("T")[0]}.tar.gz.enc`)
1542
+ .action(async (opts) => {
1543
+ try {
1544
+ const { stdout, stderr } = await runOwsCli(["backup", "--output", opts.output]);
1545
+ if (isJson())
1546
+ return printJson(jsonOk({ backup: opts.output, output: stderr || stdout }));
1547
+ if (stderr)
1548
+ console.log("\n" + stderr);
1549
+ if (stdout)
1550
+ console.log(stdout);
1551
+ console.log();
1552
+ }
1553
+ catch (e) {
1554
+ const msg = e instanceof Error ? e.message : String(e);
1555
+ if (isJson()) {
1556
+ const { jsonError } = await import("../utils.js");
1557
+ return printJson(jsonError("OWS_ERROR", msg));
1558
+ }
1559
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1560
+ process.exit(1);
1561
+ }
1562
+ });
1563
+ wallet
1564
+ .command("restore <file>")
1565
+ .description("Restore OWS vault from encrypted backup")
1566
+ .action(async (file) => {
1567
+ try {
1568
+ const { stdout, stderr } = await runOwsCli(["restore", "--input", file]);
1569
+ if (isJson())
1570
+ return printJson(jsonOk({ restored: file, output: stderr || stdout }));
1571
+ if (stderr)
1572
+ console.log("\n" + stderr);
1573
+ if (stdout)
1574
+ console.log(stdout);
1575
+ console.log();
1576
+ }
1577
+ catch (e) {
1578
+ const msg = e instanceof Error ? e.message : String(e);
1579
+ if (isJson()) {
1580
+ const { jsonError } = await import("../utils.js");
1581
+ return printJson(jsonError("OWS_ERROR", msg));
1582
+ }
1583
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1584
+ process.exit(1);
1585
+ }
1586
+ });
1587
+ // ── Key rotation (create new wallet + transfer assets) ───────────────
1588
+ wallet
1589
+ .command("rotate")
1590
+ .description("Rotate wallet keys (create new wallet + transfer assets)")
1591
+ .requiredOption("--from <name>", "Source wallet name")
1592
+ .requiredOption("--to <name>", "Target wallet name (created if not exists)")
1593
+ .option("--chain <chain>", "Chain to rotate on", "evm")
1594
+ .action(async (opts) => {
1595
+ try {
1596
+ const chainMap = { evm: "eip155:1", arbitrum: "eip155:42161", base: "eip155:8453", solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" };
1597
+ const chainArg = chainMap[opts.chain] || opts.chain;
1598
+ const { stdout, stderr } = await runOwsCli(["wallet", "rotate", "--from", opts.from, "--to", opts.to, "--chain", chainArg], 120000);
1599
+ if (isJson())
1600
+ return printJson(jsonOk({ from: opts.from, to: opts.to, chain: chainArg, output: stderr || stdout }));
1601
+ if (stderr)
1602
+ console.log("\n" + stderr);
1603
+ if (stdout)
1604
+ console.log(stdout);
1605
+ console.log();
1606
+ }
1607
+ catch (e) {
1608
+ const msg = e instanceof Error ? e.message : String(e);
1609
+ if (isJson()) {
1610
+ const { jsonError } = await import("../utils.js");
1611
+ return printJson(jsonError("OWS_ERROR", msg));
1612
+ }
1613
+ console.error(chalk.red(`\n OWS error: ${msg}\n`));
1614
+ process.exit(1);
1615
+ }
1616
+ });
1617
+ // ── Agent (DEX-side delegation) ──────────────────────────────────────
1618
+ // NOTE: Distinct from `wallet key` (OWS API token) and `wallet policy`
1619
+ // (OWS guardrail). See runOwsCli comment above for the three-way split.
1620
+ registerWalletAgentCommands(wallet, isJson);
1621
+ // ── Manage (per-DEX exchange settings — Pacifica + Lighter) ──────────
1622
+ // Absorbed from former top-level `perp manage`. Holds exchange-specific
1623
+ // settings: margin mode, subaccount, lake, builder/referral, API keys.
1624
+ // Skipped when adapter wiring isn't supplied (test programs may register
1625
+ // wallet without adapters).
1626
+ if (getAdapter && getPacificaAdapter) {
1627
+ registerWalletManageCommands(wallet, getAdapter, isJson, getPacificaAdapter);
1628
+ }
683
1629
  }