perp-cli 0.11.0 → 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 (240) 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/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
@@ -1,13 +1,51 @@
1
1
  import chalk from "chalk";
2
2
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
3
3
  import { resolve } from "path";
4
- import { createRequire } from "node:module";
5
4
  import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
6
5
  import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
7
6
  import { loadSettings, saveSettings } from "../settings.js";
8
- const _require = createRequire(import.meta.url);
9
- function loadOws() {
10
- return _require("@open-wallet-standard/core");
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() };
11
49
  }
12
50
  const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
13
51
  const WALLETS_FILE = resolve(PERP_DIR, "wallets.json");
@@ -278,7 +316,7 @@ async function _legacyImport(chain, privateKey, name, isJson) {
278
316
  }
279
317
  }
280
318
  // ── Commands ──────────────────────────────────────────────────
281
- export function registerWalletCommands(program, isJson) {
319
+ export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter) {
282
320
  const wallet = program.command("wallet").description("Wallet management & on-chain balances");
283
321
  // ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
284
322
  wallet
@@ -343,26 +381,123 @@ export function registerWalletCommands(program, isJson) {
343
381
  }
344
382
  });
345
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.
346
388
  wallet
347
- .command("import <privateKey>")
348
- .description("Import a private key into OWS encrypted vault")
389
+ .command("import [privateKey]")
390
+ .description("Import a private key into OWS encrypted vault (use --evm/--solana to import both curves at once)")
349
391
  .option("--name <name>", "Wallet alias name", "imported")
350
- .option("--chain <chain>", "Key type: evm (default) or solana", "evm")
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")
351
395
  .option("--mnemonic", "Import as mnemonic phrase instead of private key")
396
+ .option("--passphrase <pp>", "OWS vault encryption passphrase (default: empty / OWS_PASSPHRASE env)")
352
397
  .option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
353
398
  .action(async (privateKey, opts) => {
354
- // Legacy mode
399
+ // Legacy mode (positional only)
355
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`));
408
+ process.exit(1);
409
+ }
356
410
  return _legacyImport(opts.legacy, privateKey, opts.name, isJson);
357
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));
420
+ }
421
+ console.error(chalk.red(`\n Error: ${msg}\n`));
422
+ process.exit(1);
423
+ }
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
+ }
358
442
  try {
359
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"] ?? "") : "");
360
481
  let w;
361
482
  if (opts.mnemonic) {
362
- w = ows.importWalletMnemonic(opts.name, privateKey);
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");
363
497
  }
364
498
  else {
365
- w = ows.importWalletPrivateKey(opts.name, privateKey, "", undefined, opts.chain);
499
+ const keyArg = opts.chain === "solana" ? normalizeSolana(privateKey) : privateKey;
500
+ w = ows.importWalletPrivateKey(opts.name, keyArg, pp, undefined, opts.chain);
366
501
  }
367
502
  // Set as active wallet if none set
368
503
  const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
@@ -493,24 +628,21 @@ export function registerWalletCommands(program, isJson) {
493
628
  await adapter.init();
494
629
  const apiKeyIndex = 4;
495
630
  const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
496
- setEnvVar("LIGHTER_API_KEY", apiKey);
497
- setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
498
- 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, "");
499
636
  lighterApiSetup = { apiKey, accountIndex: adapter.accountIndex };
500
637
  }
501
638
  catch (e) {
502
639
  lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
503
640
  }
504
641
  }
505
- const settings = loadSettings();
506
- const referralHint = !settings.referrals
507
- ? { referrals: false, hint: "Optional: 'perp settings referrals on' to support perp-cli development (no extra fees — exchange rebates only)" }
508
- : undefined;
509
642
  if (isJson())
510
643
  return printJson(jsonOk({
511
644
  exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default,
512
645
  ...(resolved === "lighter" && { lighterApiKey: lighterApiSetup }),
513
- ...(referralHint && { referralHint }),
514
646
  }));
515
647
  console.log(chalk.green(`\n ${resolved} configured.`));
516
648
  console.log(` Address: ${chalk.green(address)}`);
@@ -523,13 +655,9 @@ export function registerWalletCommands(program, isJson) {
523
655
  }
524
656
  else {
525
657
  console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
526
- 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`));
527
659
  }
528
660
  }
529
- if (!settings.referrals) {
530
- console.log(chalk.gray(` Optional: ${chalk.cyan("perp settings referrals on")} to support perp-cli development`));
531
- console.log(chalk.gray(` (no extra fees — exchange rebates only)`));
532
- }
533
661
  console.log();
534
662
  });
535
663
  // ── show (show configured exchanges with public addresses) ──
@@ -554,14 +682,41 @@ export function registerWalletCommands(program, isJson) {
554
682
  const { valid, address } = await validateKey(entry.chain, entry.key);
555
683
  results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
556
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
+ }
557
704
  if (isJson()) {
558
705
  const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
559
- return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
706
+ return printJson(jsonOk({ envFile: ENV_FILE, owsActive, exchanges: data }));
560
707
  }
561
708
  console.log(chalk.cyan.bold("\n Configured Wallets\n"));
562
- 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) {
563
718
  console.log(chalk.gray(" No keys configured."));
564
- 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`));
565
720
  return;
566
721
  }
567
722
  for (const { name, address, source } of results) {
@@ -717,7 +872,33 @@ export function registerWalletCommands(program, isJson) {
717
872
  if (name) {
718
873
  entry = store.wallets[name];
719
874
  if (!entry) {
720
- 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
+ }
721
902
  process.exit(1);
722
903
  }
723
904
  }
@@ -898,4 +1079,551 @@ export function registerWalletCommands(program, isJson) {
898
1079
  console.log();
899
1080
  }
900
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
+ }
901
1629
  }