perp-cli 0.9.7 → 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.
- package/dist/bot/engine.js +23 -4
- package/dist/bot/presets.js +3 -3
- package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
- package/dist/bot/strategies/funding-arb-strategy.js +41 -176
- package/dist/bot/strategies/funding-arb-utils.js +9 -1
- package/dist/bot/strategies/funding-arb-v2-strategy.js +20 -11
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +2 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +26 -18
- package/dist/bot/strategies/spot-perp-executor.js +4 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/ows.d.ts +2 -0
- package/dist/commands/ows.js +887 -0
- package/dist/commands/wallet.js +378 -160
- package/dist/config.d.ts +9 -0
- package/dist/config.js +55 -9
- package/dist/exchanges/aster.js +2 -1
- package/dist/exchanges/hyperliquid.d.ts +1 -0
- package/dist/exchanges/hyperliquid.js +5 -0
- package/dist/exchanges/interface.d.ts +2 -0
- package/dist/exchanges/lighter-spot.js +5 -2
- package/dist/exchanges/lighter.js +14 -4
- package/dist/guardrail/contracts.d.ts +14 -0
- package/dist/guardrail/contracts.js +72 -0
- package/dist/guardrail/perp-guardrail.d.ts +21 -0
- package/dist/guardrail/perp-guardrail.js +137 -0
- package/dist/index.js +54 -4
- package/dist/mcp-server.js +11 -9
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +2 -0
- package/dist/signer/index.d.ts +2 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/ows-evm.d.ts +18 -0
- package/dist/signer/ows-evm.js +67 -0
- package/dist/signer/ows-loader.d.ts +2 -0
- package/dist/signer/ows-loader.js +6 -0
- package/dist/signer/ows-solana.d.ts +25 -0
- package/dist/signer/ows-solana.js +56 -0
- package/package.json +4 -2
package/dist/commands/wallet.js
CHANGED
|
@@ -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
|
-
// ──
|
|
143
|
-
|
|
144
|
-
|
|
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[
|
|
159
|
-
console.error(chalk.red(`\n Wallet "${
|
|
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[
|
|
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 =
|
|
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:
|
|
176
|
-
console.log(chalk.
|
|
177
|
-
console.log(` Name: ${chalk.white.bold(
|
|
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:
|
|
180
|
-
|
|
181
|
-
|
|
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[
|
|
194
|
-
console.error(chalk.red(`\n Wallet "${
|
|
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[
|
|
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 =
|
|
185
|
+
store.active.hyperliquid = walletName;
|
|
206
186
|
if (!store.active.lighter)
|
|
207
|
-
store.active.lighter =
|
|
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:
|
|
214
|
-
console.log(chalk.
|
|
215
|
-
console.log(` Name: ${chalk.white.bold(
|
|
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:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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[
|
|
256
|
-
console.error(chalk.red(`\n Wallet "${
|
|
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[
|
|
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 =
|
|
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
|
|
270
|
-
console.log(chalk.
|
|
271
|
-
console.log(` Name: ${chalk.white.bold(
|
|
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
|
-
|
|
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[
|
|
291
|
-
console.error(chalk.red(`\n Wallet "${
|
|
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[
|
|
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 =
|
|
263
|
+
store.active.hyperliquid = name;
|
|
300
264
|
if (!store.active.lighter)
|
|
301
|
-
store.active.lighter =
|
|
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
|
|
308
|
-
console.log(chalk.
|
|
309
|
-
console.log(` Name: ${chalk.white.bold(
|
|
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
|
-
|
|
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("
|
|
315
|
-
.description("
|
|
316
|
-
.option("--
|
|
317
|
-
.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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({
|
|
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}
|
|
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
|
|
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
|
|
482
|
-
if (isJson())
|
|
483
|
-
return printJson(jsonOk({
|
|
484
|
-
|
|
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(
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
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
|
|
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
|
}
|