perp-cli 0.9.8 → 0.11.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.
@@ -1,9 +1,14 @@
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";
4
5
  import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
5
6
  import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
6
7
  import { loadSettings, saveSettings } from "../settings.js";
8
+ const _require = createRequire(import.meta.url);
9
+ function loadOws() {
10
+ return _require("@open-wallet-standard/core");
11
+ }
7
12
  const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
8
13
  const WALLETS_FILE = resolve(PERP_DIR, "wallets.json");
9
14
  function ensurePerpDir() {
@@ -139,101 +144,69 @@ async function deriveEvmAddress(key) {
139
144
  const pk = key.startsWith("0x") ? key : `0x${key}`;
140
145
  return new ethers.Wallet(pk).address;
141
146
  }
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) => {
147
+ // ── Legacy helpers ───────────────────────────────────────────
148
+ async function _legacyGenerate(chain, name, isJson) {
149
+ if (chain === "solana") {
152
150
  const { Keypair } = await import("@solana/web3.js");
153
151
  const bs58 = (await import("bs58")).default;
154
152
  const keypair = Keypair.generate();
155
153
  const address = keypair.publicKey.toBase58();
156
154
  const privkey = bs58.encode(keypair.secretKey);
155
+ const walletName = name || "default-sol";
157
156
  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`));
157
+ if (store.wallets[walletName]) {
158
+ console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
160
159
  process.exit(1);
161
160
  }
162
- store.wallets[opts.name] = {
163
- name: opts.name,
164
- type: "solana",
165
- address,
166
- privateKey: privkey,
167
- createdAt: new Date().toISOString(),
168
- };
161
+ store.wallets[walletName] = { name: walletName, type: "solana", address, privateKey: privkey, createdAt: new Date().toISOString() };
169
162
  if (!store.active.pacifica)
170
- store.active.pacifica = opts.name;
163
+ store.active.pacifica = walletName;
171
164
  saveStore(store);
172
- // Also write to .env so loadPrivateKey finds it immediately
173
165
  setEnvVar("PACIFICA_PRIVATE_KEY", privkey);
174
166
  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)}`);
167
+ return printJson(jsonOk({ name: walletName, type: "solana", address, legacy: true }));
168
+ console.log(chalk.yellow.bold("\n [Legacy] Solana Wallet Generated\n"));
169
+ console.log(` Name: ${chalk.white.bold(walletName)}`);
178
170
  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) => {
171
+ console.log(chalk.gray(` Saved: ~/.perp/.env + wallets.json (unencrypted)`));
172
+ console.log(chalk.yellow(`\n Consider: ${chalk.cyan("perp wallet migrate")} to move to OWS vault\n`));
173
+ }
174
+ else if (chain === "evm") {
190
175
  const { ethers } = await import("ethers");
191
176
  const w = ethers.Wallet.createRandom();
177
+ const walletName = name || "default-evm";
192
178
  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`));
179
+ if (store.wallets[walletName]) {
180
+ console.error(chalk.red(`\n Wallet "${walletName}" already exists.\n`));
195
181
  process.exit(1);
196
182
  }
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
- };
183
+ store.wallets[walletName] = { name: walletName, type: "evm", address: w.address, privateKey: w.privateKey, createdAt: new Date().toISOString() };
204
184
  if (!store.active.hyperliquid)
205
- store.active.hyperliquid = opts.name;
185
+ store.active.hyperliquid = walletName;
206
186
  if (!store.active.lighter)
207
- store.active.lighter = opts.name;
187
+ store.active.lighter = walletName;
208
188
  saveStore(store);
209
- // Also write to .env so loadPrivateKey finds it immediately
210
189
  setEnvVar("HL_PRIVATE_KEY", w.privateKey);
211
190
  setEnvVar("LIGHTER_PRIVATE_KEY", w.privateKey);
212
191
  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)}`);
192
+ return printJson(jsonOk({ name: walletName, type: "evm", address: w.address, legacy: true }));
193
+ console.log(chalk.yellow.bold("\n [Legacy] EVM Wallet Generated\n"));
194
+ console.log(` Name: ${chalk.white.bold(walletName)}`);
216
195
  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;
196
+ console.log(chalk.gray(` Saved: ~/.perp/.env + wallets.json (unencrypted)`));
197
+ console.log(chalk.yellow(`\n Consider: ${chalk.cyan("perp wallet migrate")} to move to OWS vault\n`));
198
+ }
199
+ else {
200
+ console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
201
+ process.exit(1);
202
+ }
203
+ }
204
+ async function _legacyImport(chain, privateKey, name, isJson) {
205
+ if (chain === "solana") {
206
+ const { Keypair } = await import("@solana/web3.js");
207
+ const bs58 = (await import("bs58")).default;
208
+ let address, normalizedKey;
234
209
  try {
235
- const { Keypair } = await import("@solana/web3.js");
236
- const bs58 = (await import("bs58")).default;
237
210
  try {
238
211
  const bytes = bs58.decode(privateKey);
239
212
  const kp = Keypair.fromSecretKey(bytes);
@@ -250,32 +223,25 @@ export function registerWalletCommands(program, isJson) {
250
223
  catch {
251
224
  console.error(chalk.red("\n Invalid Solana private key.\n"));
252
225
  process.exit(1);
226
+ return;
253
227
  }
254
228
  const store = loadStore();
255
- if (store.wallets[opts.name]) {
256
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
229
+ if (store.wallets[name]) {
230
+ console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
257
231
  process.exit(1);
258
232
  }
259
- store.wallets[opts.name] = {
260
- name: opts.name, type: "solana", address, privateKey: normalizedKey,
261
- createdAt: new Date().toISOString(),
262
- };
233
+ store.wallets[name] = { name, type: "solana", address, privateKey: normalizedKey, createdAt: new Date().toISOString() };
263
234
  if (!store.active.pacifica)
264
- store.active.pacifica = opts.name;
235
+ store.active.pacifica = name;
265
236
  saveStore(store);
266
- // Also write to .env so loadPrivateKey finds it immediately
267
237
  setEnvVar("PACIFICA_PRIVATE_KEY", normalizedKey);
268
238
  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)}`);
239
+ return printJson(jsonOk({ name, type: "solana", address, legacy: true }));
240
+ console.log(chalk.yellow.bold("\n [Legacy] Solana Key Imported\n"));
241
+ console.log(` Name: ${chalk.white.bold(name)}`);
272
242
  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) => {
243
+ }
244
+ else if (chain === "evm") {
279
245
  const { ethers } = await import("ethers");
280
246
  const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
281
247
  let address;
@@ -285,70 +251,201 @@ export function registerWalletCommands(program, isJson) {
285
251
  catch {
286
252
  console.error(chalk.red("\n Invalid EVM private key.\n"));
287
253
  process.exit(1);
254
+ return;
288
255
  }
289
256
  const store = loadStore();
290
- if (store.wallets[opts.name]) {
291
- console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
257
+ if (store.wallets[name]) {
258
+ console.error(chalk.red(`\n Wallet "${name}" already exists.\n`));
292
259
  process.exit(1);
293
260
  }
294
- store.wallets[opts.name] = {
295
- name: opts.name, type: "evm", address, privateKey: pk,
296
- createdAt: new Date().toISOString(),
297
- };
261
+ store.wallets[name] = { name, type: "evm", address, privateKey: pk, createdAt: new Date().toISOString() };
298
262
  if (!store.active.hyperliquid)
299
- store.active.hyperliquid = opts.name;
263
+ store.active.hyperliquid = name;
300
264
  if (!store.active.lighter)
301
- store.active.lighter = opts.name;
265
+ store.active.lighter = name;
302
266
  saveStore(store);
303
- // Also write to .env so loadPrivateKey finds it immediately
304
267
  setEnvVar("HL_PRIVATE_KEY", pk);
305
268
  setEnvVar("LIGHTER_PRIVATE_KEY", pk);
306
269
  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)}`);
270
+ return printJson(jsonOk({ name, type: "evm", address, legacy: true }));
271
+ console.log(chalk.yellow.bold("\n [Legacy] EVM Key Imported\n"));
272
+ console.log(` Name: ${chalk.white.bold(name)}`);
310
273
  console.log(` Address: ${chalk.green(address)}\n`);
311
- });
312
- // ── use (set active wallet for exchange) ──
274
+ }
275
+ else {
276
+ console.error(chalk.red(`\n Unknown chain: ${chain}. Use: solana or evm\n`));
277
+ process.exit(1);
278
+ }
279
+ }
280
+ // ── Commands ──────────────────────────────────────────────────
281
+ export function registerWalletCommands(program, isJson) {
282
+ const wallet = program.command("wallet").description("Wallet management & on-chain balances");
283
+ // ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
313
284
  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`));
285
+ .command("generate [name]")
286
+ .description("Generate a new wallet (OWS encrypted vault, multi-chain)")
287
+ .option("--words <count>", "Mnemonic word count (12 or 24)", "12")
288
+ .option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
289
+ .option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
290
+ .action(async (name, opts) => {
291
+ // Legacy mode for backward compat
292
+ if (opts.legacy) {
293
+ return _legacyGenerate(opts.legacy, name, isJson);
294
+ }
295
+ const walletName = name || "default";
296
+ try {
297
+ const ows = loadOws();
298
+ const w = ows.createWallet(walletName, "", parseInt(opts.words));
299
+ // Set as active wallet
300
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
301
+ const settings = ls();
302
+ if (!settings.owsActiveWallet) {
303
+ settings.owsActiveWallet = walletName;
304
+ ss(settings);
305
+ }
306
+ if (isJson()) {
307
+ const data = {
308
+ id: w.id, name: w.name, accounts: w.accounts, createdAt: w.createdAt,
309
+ };
310
+ if (opts.showMnemonic)
311
+ data.mnemonic = ows.exportWallet(walletName);
312
+ return printJson(jsonOk(data));
313
+ }
314
+ console.log(chalk.cyan.bold("\n Wallet Created (OWS Encrypted Vault)\n"));
315
+ console.log(` Name: ${chalk.white.bold(w.name)}`);
316
+ console.log(` ID: ${chalk.gray(w.id)}`);
317
+ console.log();
318
+ const evmAcct = w.accounts.find((a) => a.chainId.startsWith("eip155:"));
319
+ const solAcct = w.accounts.find((a) => a.chainId.startsWith("solana:"));
320
+ if (evmAcct)
321
+ console.log(` EVM: ${chalk.green(evmAcct.address)}`);
322
+ if (solAcct)
323
+ console.log(` Solana: ${chalk.green(solAcct.address)}`);
324
+ console.log(chalk.gray(`\n Vault: ~/.ows/ (AES-256-GCM encrypted)`));
325
+ if (settings.owsActiveWallet === walletName) {
326
+ console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
327
+ }
328
+ if (opts.showMnemonic) {
329
+ const mnemonic = ows.exportWallet(walletName);
330
+ console.log(chalk.red.bold(`\n Mnemonic: ${mnemonic}`));
331
+ console.log(chalk.red(" Write this down and store it safely!"));
332
+ }
333
+ console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
334
+ }
335
+ catch (e) {
336
+ const msg = e instanceof Error ? e.message : String(e);
337
+ if (isJson()) {
338
+ const { jsonError } = await import("../utils.js");
339
+ return printJson(jsonError("OWS_ERROR", msg));
340
+ }
341
+ console.error(chalk.red(`\n Error: ${msg}\n`));
322
342
  process.exit(1);
323
343
  }
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`));
332
- process.exit(1);
344
+ });
345
+ // ── import (OWS by default — imports key into encrypted vault) ──
346
+ wallet
347
+ .command("import <privateKey>")
348
+ .description("Import a private key into OWS encrypted vault")
349
+ .option("--name <name>", "Wallet alias name", "imported")
350
+ .option("--chain <chain>", "Key type: evm (default) or solana", "evm")
351
+ .option("--mnemonic", "Import as mnemonic phrase instead of private key")
352
+ .option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
353
+ .action(async (privateKey, opts) => {
354
+ // Legacy mode
355
+ if (opts.legacy) {
356
+ return _legacyImport(opts.legacy, privateKey, opts.name, isJson);
357
+ }
358
+ try {
359
+ const ows = loadOws();
360
+ let w;
361
+ if (opts.mnemonic) {
362
+ w = ows.importWalletMnemonic(opts.name, privateKey);
333
363
  }
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);
364
+ else {
365
+ w = ows.importWalletPrivateKey(opts.name, privateKey, "", undefined, opts.chain);
366
+ }
367
+ // Set as active wallet if none set
368
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
369
+ const settings = ls();
370
+ if (!settings.owsActiveWallet) {
371
+ settings.owsActiveWallet = opts.name;
372
+ ss(settings);
337
373
  }
338
- exchanges = [ex];
374
+ if (isJson())
375
+ return printJson(jsonOk({
376
+ id: w.id, name: w.name, accounts: w.accounts,
377
+ }));
378
+ console.log(chalk.cyan.bold("\n Key Imported to OWS Vault\n"));
379
+ console.log(` Name: ${chalk.white.bold(w.name)}`);
380
+ const evmAcct = w.accounts.find((a) => a.chainId.startsWith("eip155:"));
381
+ const solAcct = w.accounts.find((a) => a.chainId.startsWith("solana:"));
382
+ if (evmAcct)
383
+ console.log(` EVM: ${chalk.green(evmAcct.address)}`);
384
+ if (solAcct)
385
+ console.log(` Solana: ${chalk.green(solAcct.address)}`);
386
+ console.log(chalk.gray(`\n Encrypted in ~/.ows/ vault`));
387
+ if (settings.owsActiveWallet === opts.name) {
388
+ console.log(chalk.cyan(` Active: yes (used by all exchanges)`));
389
+ }
390
+ console.log();
339
391
  }
340
- else {
341
- // Auto-detect from wallet type
342
- exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
392
+ catch (e) {
393
+ const msg = e instanceof Error ? e.message : String(e);
394
+ if (isJson()) {
395
+ const { jsonError } = await import("../utils.js");
396
+ return printJson(jsonError("OWS_ERROR", msg));
397
+ }
398
+ console.error(chalk.red(`\n Error: ${msg}\n`));
399
+ process.exit(1);
343
400
  }
401
+ });
402
+ // ── use (set active wallet — OWS first, legacy fallback) ──
403
+ wallet
404
+ .command("use <name>")
405
+ .description("Set active wallet (OWS or legacy)")
406
+ .action(async (name) => {
407
+ // Try OWS first
408
+ try {
409
+ const ows = loadOws();
410
+ const w = ows.getWallet(name);
411
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
412
+ const settings = ls();
413
+ settings.owsActiveWallet = name;
414
+ ss(settings);
415
+ const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "-";
416
+ const solAddr = w.accounts.find((a) => a.chainId.startsWith("solana:"))?.address ?? "-";
417
+ if (isJson())
418
+ return printJson(jsonOk({
419
+ wallet: name, type: "ows",
420
+ evm: evmAddr, solana: solAddr,
421
+ activeFor: "all exchanges",
422
+ }));
423
+ console.log(chalk.green(`\n ${chalk.white.bold(name)} is now the active wallet (all exchanges)`));
424
+ console.log(` EVM: ${chalk.green(evmAddr)}`);
425
+ console.log(` Solana: ${chalk.green(solAddr)}\n`);
426
+ return;
427
+ }
428
+ catch {
429
+ // Not an OWS wallet — try legacy
430
+ }
431
+ const store = loadStore();
432
+ const entry = store.wallets[name];
433
+ if (!entry) {
434
+ console.error(chalk.red(`\n Wallet "${name}" not found in OWS vault or legacy store.\n`));
435
+ console.error(chalk.gray(` Create: perp wallet generate ${name}`));
436
+ console.error(chalk.gray(` Import: perp wallet import <key> -n ${name}\n`));
437
+ process.exit(1);
438
+ }
439
+ const exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
344
440
  for (const exchange of exchanges) {
345
441
  store.active[exchange] = name;
346
442
  }
347
443
  saveStore(store);
348
444
  if (isJson())
349
- return printJson(jsonOk({ exchanges, wallet: name, address: entry.address }));
445
+ return printJson(jsonOk({ wallet: name, type: "legacy", exchanges, address: entry.address }));
350
446
  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`));
447
+ console.log(chalk.gray(` Address: ${entry.address}`));
448
+ console.log(chalk.yellow(` Note: consider migrating to OWS: perp wallet migrate\n`));
352
449
  });
353
450
  // ── set (configure exchange key → ~/.perp/.env) ──
354
451
  wallet
@@ -472,49 +569,100 @@ export function registerWalletCommands(program, isJson) {
472
569
  }
473
570
  console.log();
474
571
  });
475
- // ── list ──
572
+ // ── list (OWS + legacy) ──
476
573
  wallet
477
574
  .command("list")
478
- .description("List all saved wallets (legacy wallets.json)")
575
+ .description("List all wallets (OWS vault + legacy)")
479
576
  .action(async () => {
577
+ const { loadSettings: ls } = await import("../settings.js");
578
+ const settings = ls();
579
+ const activeOwsWallet = settings.owsActiveWallet;
580
+ // OWS wallets
581
+ let owsWallets = [];
582
+ try {
583
+ owsWallets = loadOws().listWallets();
584
+ }
585
+ catch { /* OWS not available */ }
586
+ // Legacy wallets
480
587
  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) {
588
+ const legacyEntries = Object.values(store.wallets);
589
+ if (isJson()) {
590
+ return printJson(jsonOk({
591
+ ows: { wallets: owsWallets, active: activeOwsWallet },
592
+ legacy: { wallets: store.wallets, active: store.active },
593
+ }));
594
+ }
595
+ if (owsWallets.length === 0 && legacyEntries.length === 0) {
485
596
  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"));
597
+ console.log(chalk.gray(` Create: ${chalk.cyan("perp wallet generate [name]")}`));
598
+ console.log(chalk.gray(` Import: ${chalk.cyan("perp wallet import <key>")}\n`));
487
599
  return;
488
600
  }
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));
601
+ if (owsWallets.length > 0) {
602
+ console.log(chalk.cyan.bold("\n OWS Vault Wallets") + chalk.gray(" (encrypted, multi-chain)\n"));
603
+ const rows = owsWallets.map(w => {
604
+ const evmAddr = w.accounts.find(a => a.chainId.startsWith("eip155:"))?.address ?? "-";
605
+ const solAddr = w.accounts.find(a => a.chainId.startsWith("solana:"))?.address ?? "-";
606
+ const isActive = w.name === activeOwsWallet;
607
+ return [
608
+ (isActive ? chalk.cyan("*") + " " : " ") + chalk.white.bold(w.name),
609
+ chalk.green(evmAddr.slice(0, 10) + "..." + evmAddr.slice(-4)),
610
+ chalk.green(solAddr.slice(0, 6) + "..." + solAddr.slice(-4)),
611
+ chalk.gray(w.createdAt.split("T")[0]),
612
+ ];
613
+ });
614
+ console.log(makeTable(["Name", "EVM Address", "Solana Address", "Created"], rows));
615
+ if (activeOwsWallet) {
616
+ console.log(chalk.gray(` * = active wallet`));
617
+ }
618
+ }
619
+ if (legacyEntries.length > 0) {
620
+ const activeMap = new Map();
621
+ for (const [exchange, walletName] of Object.entries(store.active)) {
622
+ if (!activeMap.has(walletName))
623
+ activeMap.set(walletName, []);
624
+ activeMap.get(walletName).push(exchange);
625
+ }
626
+ console.log(chalk.yellow.bold("\n Legacy Wallets") + chalk.gray(" (unencrypted, ~/.perp/wallets.json)\n"));
627
+ const rows = legacyEntries.map(w => {
628
+ const activeFor = activeMap.get(w.name) ?? [];
629
+ const activeStr = activeFor.length ? chalk.cyan(activeFor.join(", ")) : chalk.gray("-");
630
+ return [
631
+ chalk.white.bold(w.name),
632
+ w.type,
633
+ chalk.green(w.address.slice(0, 10) + "..." + w.address.slice(-6)),
634
+ activeStr,
635
+ chalk.gray(w.createdAt.split("T")[0]),
636
+ ];
637
+ });
638
+ console.log(makeTable(["Name", "Type", "Address", "Active For", "Created"], rows));
639
+ console.log(chalk.yellow(` Tip: migrate to OWS with ${chalk.cyan("perp wallet migrate")}`));
640
+ }
511
641
  console.log();
512
642
  });
513
- // ── remove ──
643
+ // ── remove (OWS + legacy) ──
514
644
  wallet
515
645
  .command("remove <name>")
516
- .description("Remove a saved wallet")
646
+ .description("Remove a wallet (OWS or legacy)")
517
647
  .action(async (name) => {
648
+ // Try OWS first
649
+ try {
650
+ const ows = loadOws();
651
+ const w = ows.getWallet(name);
652
+ ows.deleteWallet(name);
653
+ // Clear active if this was the active wallet
654
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
655
+ const settings = ls();
656
+ if (settings.owsActiveWallet === name) {
657
+ settings.owsActiveWallet = "";
658
+ ss(settings);
659
+ }
660
+ if (isJson())
661
+ return printJson(jsonOk({ removed: name, id: w.id, type: "ows" }));
662
+ console.log(chalk.yellow(`\n OWS wallet "${name}" removed from vault.\n`));
663
+ return;
664
+ }
665
+ catch { /* not in OWS — try legacy */ }
518
666
  const store = loadStore();
519
667
  if (!store.wallets[name]) {
520
668
  console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
@@ -522,15 +670,14 @@ export function registerWalletCommands(program, isJson) {
522
670
  }
523
671
  const address = store.wallets[name].address;
524
672
  delete store.wallets[name];
525
- // Clear active references
526
673
  for (const [exchange, walletName] of Object.entries(store.active)) {
527
674
  if (walletName === name)
528
675
  delete store.active[exchange];
529
676
  }
530
677
  saveStore(store);
531
678
  if (isJson())
532
- return printJson(jsonOk({ removed: name, address }));
533
- console.log(chalk.yellow(`\n Wallet "${name}" removed.`));
679
+ return printJson(jsonOk({ removed: name, address, type: "legacy" }));
680
+ console.log(chalk.yellow(`\n Legacy wallet "${name}" removed.`));
534
681
  console.log(chalk.gray(` Address was: ${address}\n`));
535
682
  });
536
683
  // ── rename ──
@@ -680,4 +827,75 @@ export function registerWalletCommands(program, isJson) {
680
827
  console.log(makeTable(["Token", "Balance", "USD Value"], rows));
681
828
  console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
682
829
  });
830
+ // ── migrate (legacy → OWS) ──
831
+ wallet
832
+ .command("migrate")
833
+ .description("Migrate legacy wallets (wallets.json) to OWS encrypted vault")
834
+ .action(async () => {
835
+ const store = loadStore();
836
+ const entries = Object.values(store.wallets);
837
+ if (entries.length === 0) {
838
+ if (isJson())
839
+ return printJson(jsonOk({ migrated: 0 }));
840
+ console.log(chalk.gray("\n No legacy wallets to migrate.\n"));
841
+ return;
842
+ }
843
+ const ows = loadOws();
844
+ const results = [];
845
+ for (const entry of entries) {
846
+ try {
847
+ try {
848
+ ows.getWallet(entry.name);
849
+ results.push({ name: entry.name, status: "skipped (already in OWS)" });
850
+ continue;
851
+ }
852
+ catch { /* good — doesn't exist */ }
853
+ const chain = entry.type === "solana" ? "solana" : "evm";
854
+ let pk = entry.privateKey;
855
+ // OWS expects hex private keys — convert bs58 Solana keys
856
+ if (chain === "solana" && !pk.startsWith("0x")) {
857
+ try {
858
+ const bs58 = (await import("bs58")).default;
859
+ const bytes = bs58.decode(pk);
860
+ const keyBytes = bytes.length === 64 ? bytes.slice(0, 32) : bytes;
861
+ pk = "0x" + Buffer.from(keyBytes).toString("hex");
862
+ }
863
+ catch {
864
+ // If it's already hex or a JSON array, try as-is
865
+ }
866
+ }
867
+ ows.importWalletPrivateKey(entry.name, pk, "", undefined, chain);
868
+ results.push({ name: entry.name, status: "migrated" });
869
+ }
870
+ catch (e) {
871
+ results.push({ name: entry.name, status: "failed", error: e instanceof Error ? e.message : String(e) });
872
+ }
873
+ }
874
+ // Set first migrated wallet as active if none set
875
+ const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
876
+ const settings = ls();
877
+ if (!settings.owsActiveWallet) {
878
+ const firstMigrated = results.find(r => r.status === "migrated");
879
+ if (firstMigrated) {
880
+ settings.owsActiveWallet = firstMigrated.name;
881
+ ss(settings);
882
+ }
883
+ }
884
+ if (isJson())
885
+ return printJson(jsonOk({ results }));
886
+ console.log(chalk.cyan.bold("\n Migration Results\n"));
887
+ for (const r of results) {
888
+ const icon = r.status === "migrated" ? chalk.green("OK") : r.status.startsWith("skipped") ? chalk.yellow("SKIP") : chalk.red("FAIL");
889
+ console.log(` ${icon} ${chalk.white.bold(r.name)} — ${r.status}${r.error ? ` (${r.error})` : ""}`);
890
+ }
891
+ const migrated = results.filter(r => r.status === "migrated").length;
892
+ if (migrated > 0) {
893
+ console.log(chalk.green(`\n ${migrated} wallet(s) migrated to OWS vault.`));
894
+ console.log(chalk.gray(" Keys are now encrypted at ~/.ows/"));
895
+ console.log(chalk.yellow(" You can remove legacy wallets.json after verifying.\n"));
896
+ }
897
+ else {
898
+ console.log();
899
+ }
900
+ });
683
901
  }