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.
package/dist/config.d.ts CHANGED
@@ -5,6 +5,15 @@ export type Exchange = "pacifica" | "hyperliquid" | "lighter" | "aster";
5
5
  * Returns null if no key is configured.
6
6
  */
7
7
  export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string | null>;
8
+ /**
9
+ * Resolve an OWS wallet name to a signer-ready marker.
10
+ * Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
11
+ */
12
+ export declare function resolveOwsWallet(walletName: string): string | null;
13
+ /** Check if a key string represents an OWS wallet reference. */
14
+ export declare function isOwsKey(key: string): boolean;
15
+ /** Extract OWS wallet name from an ows: prefixed key string. */
16
+ export declare function getOwsWalletName(key: string): string;
8
17
  export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string>;
9
18
  export declare function parseSolanaKeypair(input: string): Keypair;
10
19
  export declare function isEvmPrivateKey(input: string): boolean;
package/dist/config.js CHANGED
@@ -3,6 +3,8 @@ import bs58 from "bs58";
3
3
  import chalk from "chalk";
4
4
  import { resolve } from "path";
5
5
  import { existsSync, readFileSync } from "fs";
6
+ import { createRequire } from "node:module";
7
+ const _require = createRequire(import.meta.url);
6
8
  /**
7
9
  * Try to load a private key without throwing if none is found.
8
10
  * Returns null if no key is configured.
@@ -15,9 +17,40 @@ export async function tryLoadPrivateKey(exchange, pkOverride, walletName) {
15
17
  return null;
16
18
  }
17
19
  }
20
+ /**
21
+ * Resolve an OWS wallet name to a signer-ready marker.
22
+ * Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
23
+ */
24
+ export function resolveOwsWallet(walletName) {
25
+ try {
26
+ const { getWallet } = _require("@open-wallet-standard/core");
27
+ getWallet(walletName); // throws if not found
28
+ return `ows:${walletName}`;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** Check if a key string represents an OWS wallet reference. */
35
+ export function isOwsKey(key) {
36
+ return key.startsWith("ows:");
37
+ }
38
+ /** Extract OWS wallet name from an ows: prefixed key string. */
39
+ export function getOwsWalletName(key) {
40
+ return key.slice(4);
41
+ }
18
42
  export async function loadPrivateKey(exchange, pkOverride, walletName) {
19
- // 0. Wallet name override (--wallet flag)
43
+ // 0a. OWS wallet (--ows flag sets walletName with "ows:" prefix)
44
+ if (walletName?.startsWith("ows:")) {
45
+ return walletName; // pass through — adapters detect the ows: prefix
46
+ }
47
+ // 0b. Wallet name override (--wallet flag) — check OWS first, then local
20
48
  if (walletName) {
49
+ // Try OWS vault
50
+ const owsKey = resolveOwsWallet(walletName);
51
+ if (owsKey)
52
+ return owsKey;
53
+ // Fall back to local wallets
21
54
  const { getWalletKeyByName } = await import("./commands/wallet.js");
22
55
  const key = getWalletKeyByName(walletName);
23
56
  if (!key)
@@ -27,7 +60,20 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
27
60
  // 1. CLI flag
28
61
  if (pkOverride)
29
62
  return pkOverride;
30
- // 2. Exchange-specific env vars
63
+ // 2. OWS active wallet (highest priority after explicit flags)
64
+ try {
65
+ const { loadSettings } = await import("./settings.js");
66
+ const settings = loadSettings();
67
+ if (settings.owsActiveWallet) {
68
+ const owsKey = resolveOwsWallet(settings.owsActiveWallet);
69
+ if (owsKey)
70
+ return owsKey;
71
+ }
72
+ }
73
+ catch {
74
+ // settings module not available, skip
75
+ }
76
+ // 3. Exchange-specific env vars
31
77
  const envMap = {
32
78
  pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
33
79
  hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
@@ -38,10 +84,10 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
38
84
  if (process.env[envVar])
39
85
  return process.env[envVar];
40
86
  }
41
- // 3. Generic fallback
87
+ // 4. Generic fallback
42
88
  if (process.env.PRIVATE_KEY)
43
89
  return process.env.PRIVATE_KEY;
44
- // 4. Active wallet from wallets.json
90
+ // 5. Legacy: Active wallet from wallets.json
45
91
  try {
46
92
  const { getActiveWalletKey } = await import("./commands/wallet.js");
47
93
  const walletKey = getActiveWalletKey(exchange);
@@ -56,12 +102,12 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
56
102
  if (existsSync(keyFile)) {
57
103
  return readFileSync(keyFile, "utf-8").trim();
58
104
  }
59
- throw new Error(`No private key configured for ${exchange}.\n\n` +
60
- ` Quick start: ${chalk.cyan("perp setup")}\n\n` +
105
+ throw new Error(`No wallet configured for ${exchange}.\n\n` +
106
+ ` Quick start: ${chalk.cyan("perp wallet generate")}\n\n` +
61
107
  ` Or manually:\n` +
62
- ` perp wallet set ${exchange} <key>\n` +
63
- ` ${envMap[exchange][0]}=<key> (env var)\n` +
64
- ` --private-key <key> (per-command)`);
108
+ ` perp wallet import <key> Import key to OWS vault\n` +
109
+ ` perp wallet set ${exchange} <key> Legacy: set env var\n` +
110
+ ` --private-key <key> Per-command override`);
65
111
  }
66
112
  export function parseSolanaKeypair(input) {
67
113
  // Try base58 first
@@ -55,6 +55,7 @@ export class HyperliquidAdapter {
55
55
  /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
56
56
  setSigner(signer) {
57
57
  this._evmSigner = signer;
58
+ this._address = signer.getAddress();
58
59
  }
59
60
  async init() {
60
61
  // Suppress "WebSocket connected" noise from hyperliquid SDK
@@ -71,6 +72,9 @@ export class HyperliquidAdapter {
71
72
  this._evmSigner = await LocalEvmSigner.create(this._privateKey);
72
73
  this._address = this._evmSigner.getAddress();
73
74
  }
75
+ else if (this._evmSigner && !this._address) {
76
+ this._address = this._evmSigner.getAddress();
77
+ }
74
78
  // Build asset index map
75
79
  await this._loadAssetMap();
76
80
  }
@@ -528,8 +528,11 @@ export class LighterSpotAdapter {
528
528
  for (const [k, v] of Object.entries(params))
529
529
  url.searchParams.set(k, v);
530
530
  const res = await fetch(url.toString());
531
- if (!res.ok)
532
- throw new Error(`GET ${path} failed (${res.status}): ${await res.text()}`);
531
+ if (!res.ok) {
532
+ const text = await res.text();
533
+ const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
534
+ throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
535
+ }
533
536
  return res.json();
534
537
  }
535
538
  async _restGetAuth(path, params) {
@@ -64,6 +64,7 @@ export class LighterAdapter {
64
64
  /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
65
65
  setSigner(signer) {
66
66
  this._evmSigner = signer;
67
+ this._address = signer.getAddress();
67
68
  }
68
69
  async init() {
69
70
  // Initialize EVM signer if not externally injected (skip if no key — read-only mode)
@@ -71,6 +72,9 @@ export class LighterAdapter {
71
72
  this._evmSigner = await LocalEvmSigner.create(this._evmKey);
72
73
  this._address = this._evmSigner.getAddress();
73
74
  }
75
+ else if (this._evmSigner && !this._address) {
76
+ this._address = this._evmSigner.getAddress();
77
+ }
74
78
  // Fetch account index from REST API (skip if no address — read-only mode)
75
79
  if (!this._address) {
76
80
  await this._refreshMarketMap();
@@ -961,8 +965,11 @@ export class LighterAdapter {
961
965
  is_external_deposit: "true",
962
966
  }),
963
967
  });
964
- if (!res.ok)
965
- throw new Error(`createIntentAddress failed: ${await res.text()}`);
968
+ if (!res.ok) {
969
+ const text = await res.text();
970
+ const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
971
+ throw new Error(`createIntentAddress failed: ${clean}`);
972
+ }
966
973
  return res.json();
967
974
  }
968
975
  /**
@@ -1035,8 +1042,11 @@ export class LighterAdapter {
1035
1042
  for (const [k, v] of Object.entries(params))
1036
1043
  url.searchParams.set(k, v);
1037
1044
  const res = await fetch(url.toString());
1038
- if (!res.ok)
1039
- throw new Error(`GET ${path} failed (${res.status}): ${await res.text()}`);
1045
+ if (!res.ok) {
1046
+ const text = await res.text();
1047
+ const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
1048
+ throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
1049
+ }
1040
1050
  return res.json();
1041
1051
  }
1042
1052
  /** Generate a unique client order index (uint48). */
@@ -0,0 +1,14 @@
1
+ /**
2
+ * DEX contract whitelist for perp-guardrail policy executable.
3
+ * Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
4
+ */
5
+ export declare const USDC_ADDRESSES: Record<string, string>;
6
+ export declare const ALLOWED_CONTRACTS: Record<string, string[]>;
7
+ export declare const ALLOWED_CHAINS: string[];
8
+ export declare const DEFAULT_LIMITS: {
9
+ max_tx_usd: number;
10
+ max_daily_usd: number;
11
+ max_withdraw_usd: number;
12
+ max_daily_withdraw_usd: number;
13
+ };
14
+ export declare const DANGEROUS_SELECTORS: string[];
@@ -0,0 +1,72 @@
1
+ /**
2
+ * DEX contract whitelist for perp-guardrail policy executable.
3
+ * Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
4
+ */
5
+ // ── Pacifica (Solana) ──
6
+ const PACIFICA_MAINNET = [
7
+ "PCFA5iYgmqK6MqPhWNKg7Yv7auX7VZ4Cx7T1eJyrAMH", // Pacifica program
8
+ "9Gdmhq4Gv1LnNMp7aiS1HSVd7pNnXNMsbuXALCQRmGjY", // Central state
9
+ "72R843XwZxqWhsJceARQQTTbYtWy6Zw9et2YV4FpRHTa", // Vault
10
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
11
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
12
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token
13
+ "11111111111111111111111111111111", // System Program
14
+ "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", // CCTP V2 Token Messenger
15
+ "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", // CCTP V2 Message Transmitter
16
+ ];
17
+ const PACIFICA_TESTNET = [
18
+ "peRPsYCcB1J9jvrs29jiGdjkytxs8uHLmSPLKKP9ptm", // Testnet program
19
+ "2zPRq1Qvdq5A4Ld6WsH7usgCge4ApZRYfhhf5VAjfXxv", // Testnet central state
20
+ "5SDFdHZGTZbyRYu54CgmRkCGnPHC5pYaN27p7XGLqnBs", // Testnet vault
21
+ "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM", // Testnet USDC
22
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
23
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
24
+ "11111111111111111111111111111111",
25
+ ];
26
+ // ── Hyperliquid (Arbitrum + HyperCore) ──
27
+ const HYPERLIQUID_CONTRACTS = [
28
+ "0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757", // CctpForwarder (HyperCore)
29
+ ];
30
+ // ── CCTP V2 (shared across EVM chains) ──
31
+ const CCTP_V2_EVM = [
32
+ "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", // TokenMessengerV2
33
+ "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", // MessageTransmitterV2
34
+ ];
35
+ // ── USDC addresses ──
36
+ export const USDC_ADDRESSES = {
37
+ "eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum
38
+ "eip155:42161": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum
39
+ "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
40
+ };
41
+ // ── Complete whitelist by CAIP-2 chain ──
42
+ export const ALLOWED_CONTRACTS = {
43
+ // Solana mainnet
44
+ "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": PACIFICA_MAINNET,
45
+ // Solana devnet
46
+ "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": PACIFICA_TESTNET,
47
+ // Arbitrum (Hyperliquid deposits + CCTP)
48
+ "eip155:42161": [...HYPERLIQUID_CONTRACTS, ...CCTP_V2_EVM, USDC_ADDRESSES["eip155:42161"]],
49
+ // Hyperliquid signing domain (phantom chain for typed data)
50
+ "eip155:1337": [], // no contract calls, only typed data signing
51
+ // Base (CCTP bridge + USDC)
52
+ "eip155:8453": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:8453"]],
53
+ // Lighter (ZK rollup)
54
+ "eip155:304": [], // signing-based, no direct contract interaction
55
+ "eip155:300": [], // Lighter testnet
56
+ // Ethereum mainnet (CCTP + USDC)
57
+ "eip155:1": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:1"]],
58
+ };
59
+ // ── Allowed chain IDs (all chains our DEXes operate on) ──
60
+ export const ALLOWED_CHAINS = Object.keys(ALLOWED_CONTRACTS);
61
+ // ── Default guardrail limits ──
62
+ export const DEFAULT_LIMITS = {
63
+ max_tx_usd: 1000,
64
+ max_daily_usd: 5000,
65
+ max_withdraw_usd: 500,
66
+ max_daily_withdraw_usd: 2000,
67
+ };
68
+ // ── Common blocked selectors ──
69
+ export const DANGEROUS_SELECTORS = [
70
+ "0x095ea7b3", // approve(address,uint256) — unlimited token approvals
71
+ "0x23b872dd", // transferFrom(address,address,uint256)
72
+ ];
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * perp-guardrail — OWS Policy Engine custom executable.
4
+ *
5
+ * Protocol:
6
+ * stdin → PolicyContext JSON (from OWS engine)
7
+ * stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
8
+ *
9
+ * Validates:
10
+ * 1. Contract address whitelist (per chain)
11
+ * 2. Per-transaction USD amount limit
12
+ * 3. Daily cumulative spending limit
13
+ * 4. Withdrawal limits (per-tx and daily)
14
+ * 5. Blocked function selectors (e.g. approve)
15
+ * 6. Allowed trading symbols
16
+ *
17
+ * Configuration is read from PolicyContext.policy_config:
18
+ * { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
19
+ * allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
20
+ */
21
+ export {};
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * perp-guardrail — OWS Policy Engine custom executable.
4
+ *
5
+ * Protocol:
6
+ * stdin → PolicyContext JSON (from OWS engine)
7
+ * stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
8
+ *
9
+ * Validates:
10
+ * 1. Contract address whitelist (per chain)
11
+ * 2. Per-transaction USD amount limit
12
+ * 3. Daily cumulative spending limit
13
+ * 4. Withdrawal limits (per-tx and daily)
14
+ * 5. Blocked function selectors (e.g. approve)
15
+ * 6. Allowed trading symbols
16
+ *
17
+ * Configuration is read from PolicyContext.policy_config:
18
+ * { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
19
+ * allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
20
+ */
21
+ import { ALLOWED_CONTRACTS, ALLOWED_CHAINS, DEFAULT_LIMITS } from "./contracts.js";
22
+ function deny(reason) {
23
+ return { allow: false, reason };
24
+ }
25
+ function allow() {
26
+ return { allow: true };
27
+ }
28
+ function weiToUsd(weiStr, ethPriceUsd) {
29
+ const wei = BigInt(weiStr || "0");
30
+ const ethValue = Number(wei) / 1e18;
31
+ return ethValue * ethPriceUsd;
32
+ }
33
+ // Well-known EVM function selectors
34
+ const KNOWN_SELECTORS = {
35
+ "0x095ea7b3": "approve",
36
+ "0x23b872dd": "transferFrom",
37
+ "0xa9059cbb": "transfer",
38
+ "0x3ccfd60b": "withdraw",
39
+ "0x2e1a7d4d": "withdraw(uint256)",
40
+ "0xd0e30db0": "deposit",
41
+ };
42
+ function isWithdrawTx(data) {
43
+ if (!data || data === "0x")
44
+ return false;
45
+ const selector = data.slice(0, 10).toLowerCase();
46
+ return selector === "0x3ccfd60b" || selector === "0x2e1a7d4d";
47
+ }
48
+ function evaluate(ctx) {
49
+ const config = ctx.policy_config ?? {};
50
+ const maxTxUsd = config.max_tx_usd ?? DEFAULT_LIMITS.max_tx_usd;
51
+ const maxDailyUsd = config.max_daily_usd ?? DEFAULT_LIMITS.max_daily_usd;
52
+ const maxWithdrawUsd = config.max_withdraw_usd ?? maxTxUsd;
53
+ const maxDailyWithdrawUsd = config.max_daily_withdraw_usd ?? maxDailyUsd;
54
+ const contracts = config.allowed_contracts ?? ALLOWED_CONTRACTS;
55
+ const chains = config.allowed_chains ?? ALLOWED_CHAINS;
56
+ const ethPrice = config.eth_price_usd ?? 3500;
57
+ const blockSelectors = config.block_selectors ?? [];
58
+ // 1. Chain check
59
+ if (!chains.includes(ctx.chain_id)) {
60
+ return deny(`chain ${ctx.chain_id} not in allowed list`);
61
+ }
62
+ // 2. Contract whitelist (skip for signing-only chains like eip155:1337, eip155:304)
63
+ const chainContracts = contracts[ctx.chain_id];
64
+ if (chainContracts && chainContracts.length > 0 && ctx.transaction.to) {
65
+ const target = ctx.transaction.to.toLowerCase();
66
+ const allowed = chainContracts.map(a => a.toLowerCase());
67
+ if (!allowed.includes(target)) {
68
+ return deny(`contract ${ctx.transaction.to} not whitelisted for ${ctx.chain_id}`);
69
+ }
70
+ }
71
+ // 3. Blocked function selectors (e.g. approve, transferFrom)
72
+ if (blockSelectors.length > 0 && ctx.transaction.data && ctx.transaction.data !== "0x") {
73
+ const selector = ctx.transaction.data.slice(0, 10).toLowerCase();
74
+ if (blockSelectors.map(s => s.toLowerCase()).includes(selector)) {
75
+ const name = KNOWN_SELECTORS[selector] || selector;
76
+ return deny(`function ${name} is blocked by policy`);
77
+ }
78
+ }
79
+ // 4. Per-transaction amount
80
+ if (ctx.transaction.value && ctx.transaction.value !== "0") {
81
+ const txUsd = weiToUsd(ctx.transaction.value, ethPrice);
82
+ if (txUsd > maxTxUsd) {
83
+ return deny(`tx value $${txUsd.toFixed(2)} exceeds limit $${maxTxUsd}`);
84
+ }
85
+ }
86
+ // 5. Withdrawal-specific limits
87
+ if (isWithdrawTx(ctx.transaction.data)) {
88
+ if (ctx.transaction.value && ctx.transaction.value !== "0") {
89
+ const withdrawUsd = weiToUsd(ctx.transaction.value, ethPrice);
90
+ if (withdrawUsd > maxWithdrawUsd) {
91
+ return deny(`withdrawal $${withdrawUsd.toFixed(2)} exceeds per-tx limit $${maxWithdrawUsd}`);
92
+ }
93
+ }
94
+ // Daily withdrawal check uses same daily_total (OWS tracks cumulative)
95
+ if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
96
+ const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
97
+ if (dailyUsd > maxDailyWithdrawUsd) {
98
+ return deny(`daily withdrawals $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyWithdrawUsd}`);
99
+ }
100
+ }
101
+ }
102
+ // 6. Daily spending limit (all operations)
103
+ if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
104
+ const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
105
+ if (dailyUsd > maxDailyUsd) {
106
+ return deny(`daily spending $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyUsd}`);
107
+ }
108
+ }
109
+ return allow();
110
+ }
111
+ // ── Main: read stdin, evaluate, write stdout ──
112
+ async function main() {
113
+ const chunks = [];
114
+ for await (const chunk of process.stdin) {
115
+ chunks.push(chunk);
116
+ }
117
+ const input = Buffer.concat(chunks).toString("utf-8").trim();
118
+ if (!input) {
119
+ process.stdout.write(JSON.stringify(deny("empty stdin")));
120
+ process.exit(0);
121
+ }
122
+ let ctx;
123
+ try {
124
+ ctx = JSON.parse(input);
125
+ }
126
+ catch {
127
+ process.stdout.write(JSON.stringify(deny("invalid JSON on stdin")));
128
+ process.exit(0);
129
+ }
130
+ const result = evaluate(ctx);
131
+ process.stdout.write(JSON.stringify(result));
132
+ process.exit(0);
133
+ }
134
+ main().catch((e) => {
135
+ process.stdout.write(JSON.stringify(deny(`internal error: ${e instanceof Error ? e.message : String(e)}`)));
136
+ process.exit(0);
137
+ });
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ config();
10
10
  import { Command } from "commander";
11
11
  import chalk from "chalk";
12
12
  import { Keypair } from "@solana/web3.js";
13
- import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
13
+ import { tryLoadPrivateKey, parseSolanaKeypair, isOwsKey, getOwsWalletName } from "./config.js";
14
14
  import { PacificaAdapter } from "./exchanges/pacifica.js";
15
15
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
16
16
  import { resolveExchangeName } from "./exchanges/registry.js";
@@ -21,6 +21,7 @@ import { registerManageCommands } from "./commands/manage.js";
21
21
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
22
22
  import { registerArbCommands } from "./commands/arb.js";
23
23
  import { registerWalletCommands } from "./commands/wallet.js";
24
+ import { registerOwsCommands } from "./commands/ows.js";
24
25
  import { registerBridgeCommands } from "./commands/bridge.js";
25
26
  // deposit + withdraw merged into funds
26
27
  import { registerFundsCommands } from "./commands/funds.js";
@@ -60,6 +61,8 @@ program
60
61
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
61
62
  .option("--dry-run", "Simulate trades without executing (log as simulated)")
62
63
  .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
64
+ .option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
65
+ .option("--ows-key <token>", "OWS API key token (ows_key_...) for policy-gated agent access")
63
66
  .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
64
67
  .configureOutput({
65
68
  writeErr: (str) => {
@@ -90,8 +93,10 @@ function getExchange() {
90
93
  }
91
94
  async function getAdapter() {
92
95
  const opts = program.opts();
93
- const walletName = opts.wallet;
94
- // Skip cache when --wallet is specified (different wallet = different account)
96
+ const owsName = opts.ows;
97
+ const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
98
+ const walletName = owsName ? `ows:${owsName}` : opts.wallet;
99
+ // Skip cache when --wallet/--ows is specified (different wallet = different account)
95
100
  if (!walletName && _adapter)
96
101
  return _adapter;
97
102
  const exchange = resolveExchangeAlias(opts.exchange);
@@ -99,6 +104,10 @@ async function getAdapter() {
99
104
  const isTestnet = network === "testnet";
100
105
  // Try to load key — null means no key configured (read-only mode)
101
106
  const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
107
+ // OWS wallet detected — use OWS signers instead of raw keys
108
+ if (pk && isOwsKey(pk)) {
109
+ return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
110
+ }
102
111
  switch (exchange) {
103
112
  case "pacifica": {
104
113
  const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
@@ -170,6 +179,44 @@ async function getAdapter() {
170
179
  }
171
180
  return _adapter;
172
181
  }
182
+ /** Initialize adapter with OWS wallet signer injection. */
183
+ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToken) {
184
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
185
+ const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
186
+ // If an OWS API key token is provided, use it as passphrase → routes through policy engine
187
+ const passphrase = owsKeyToken || "";
188
+ switch (exchange) {
189
+ case "pacifica": {
190
+ const pacNetwork = (isTestnet ? "testnet" : "mainnet");
191
+ const settings = loadSettings();
192
+ const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
193
+ const dummyKeypair = Keypair.generate();
194
+ _pacificaAdapter = new PacificaAdapter(dummyKeypair, pacNetwork, builderCode, true);
195
+ _pacificaAdapter.setSigner(OwsSolanaSigner.create(owsWalletName, passphrase));
196
+ _adapter = _pacificaAdapter;
197
+ return _adapter;
198
+ }
199
+ case "hyperliquid": {
200
+ _hlAdapter = new HyperliquidAdapter(undefined, isTestnet);
201
+ if (opts.dex)
202
+ _hlAdapter.setDex(opts.dex);
203
+ _hlAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
204
+ await _hlAdapter.init();
205
+ _adapter = _hlAdapter;
206
+ return _adapter;
207
+ }
208
+ case "lighter": {
209
+ const { LighterAdapter } = await import("./exchanges/lighter.js");
210
+ _lighterAdapter = new LighterAdapter("", isTestnet);
211
+ _lighterAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
212
+ await _lighterAdapter.init();
213
+ _adapter = _lighterAdapter;
214
+ return _adapter;
215
+ }
216
+ default:
217
+ throw new Error(`Unknown exchange: ${exchange}`);
218
+ }
219
+ }
173
220
  // Sync wrapper for commands that need adapter (lazy init)
174
221
  function getAdapterSync() {
175
222
  if (!_adapter)
@@ -201,6 +248,7 @@ registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
201
248
  // stream commands removed
202
249
  registerArbCommands(program, isJson, getAdapterForExchange);
203
250
  registerWalletCommands(program, isJson);
251
+ registerOwsCommands(program, isJson);
204
252
  registerBridgeCommands(program, isJson);
205
253
  registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
206
254
  // alert commands removed
@@ -632,6 +680,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
632
680
  const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
633
681
  process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
634
682
  process.env.LIGHTER_PRIVATE_KEY);
683
+ const hasOwsWallet = !!settings.owsActiveWallet;
635
684
  // ── ASCII banner ──
636
685
  const banner = [
637
686
  "",
@@ -645,9 +694,10 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
645
694
  chalk.gray(` 4 exchanges · 19 strategies · v${_pkg.version}`),
646
695
  ];
647
696
  console.log(banner.join("\n"));
648
- if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
697
+ if (!status.hasWallets && !hasEnvKey && !hasOwsWallet && !settings.defaultExchange) {
649
698
  // Fresh install — onboarding
650
699
  console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
700
+ console.log(` ${chalk.cyan("perp wallet generate")} create OWS encrypted wallet`);
651
701
  console.log(` ${chalk.cyan("perp setup")} setup wizard`);
652
702
  console.log(chalk.gray(`\n Explore without a wallet:`));
653
703
  console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
@@ -878,11 +878,11 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
878
878
  else if (category === "wallet") {
879
879
  explanation = {
880
880
  command,
881
- description: { show: "Show configured wallets with public addresses", list: "List configured wallets", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new wallet keypair", import: "Import an existing private key", set: "Set private key for an exchange", use: "Set active wallet", remove: "Remove a wallet", rename: "Rename a wallet" }[sub] || `Wallet: ${sub}`,
881
+ description: { show: "Show configured wallets with public addresses", list: "List all wallets (OWS vault + legacy)", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new OWS wallet (encrypted, multi-chain)", import: "Import key into OWS vault", migrate: "Migrate legacy wallets to OWS encrypted vault", set: "Set private key for an exchange (legacy)", use: "Set active wallet (OWS or legacy)", remove: "Remove a wallet", rename: "Rename a wallet", ows: "OWS vault management (create, list, info, delete)" }[sub] || `Wallet: ${sub}`,
882
882
  parameters: [],
883
- risks: sub === "generate" ? ["Store the private key securely — it cannot be recovered"] : sub === "remove" ? ["Wallet will be removed — ensure you have the private key backed up"] : [],
883
+ risks: sub === "generate" ? ["Save the mnemonic securely — it cannot be recovered. Keys are stored in ~/.ows/ encrypted vault."] : sub === "remove" ? ["Wallet will be removed — ensure you have backups"] : [],
884
884
  category: "read",
885
- relatedCommands: ["perp wallet show", "perp wallet balance"],
885
+ relatedCommands: ["perp wallet list", "perp wallet balance", "perp wallet ows list"],
886
886
  };
887
887
  }
888
888
  else if (category === "backtest") {
@@ -1124,17 +1124,19 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
1124
1124
  },
1125
1125
  },
1126
1126
  wallet: {
1127
- description: "Wallet management",
1127
+ description: "Wallet management (OWS encrypted vault + legacy)",
1128
1128
  subcommands: {
1129
1129
  show: { usage: "perp wallet show", description: "Show configured wallets with public addresses" },
1130
- list: { usage: "perp wallet list", description: "List wallets" },
1130
+ list: { usage: "perp wallet list", description: "List all wallets (OWS vault + legacy)" },
1131
1131
  balance: { usage: "perp wallet balance [name]", description: "On-chain balances" },
1132
- generate: { usage: "perp wallet generate solana|evm", description: "Generate new wallet" },
1133
- import: { usage: "perp wallet import solana|evm <privateKey>", description: "Import existing private key" },
1134
- set: { usage: "perp wallet set <exchange> <key>", description: "Set private key for exchange" },
1135
- use: { usage: "perp wallet use <name> [exchange]", description: "Set active wallet" },
1132
+ generate: { usage: "perp wallet generate [name]", description: "Generate new OWS wallet (encrypted, multi-chain)" },
1133
+ import: { usage: "perp wallet import <privateKey>", description: "Import key into OWS vault" },
1134
+ migrate: { usage: "perp wallet migrate", description: "Migrate legacy wallets to OWS vault" },
1135
+ set: { usage: "perp wallet set <exchange> <key>", description: "Set private key for exchange (legacy)" },
1136
+ use: { usage: "perp wallet use <name>", description: "Set active wallet (OWS or legacy)" },
1136
1137
  remove: { usage: "perp wallet remove <name>", description: "Remove a wallet" },
1137
1138
  rename: { usage: "perp wallet rename <oldName> <newName>", description: "Rename a wallet" },
1139
+ ows: { usage: "perp wallet ows <sub>", description: "OWS vault: create, list, info, delete, policy, key, fund, pay, setup" },
1138
1140
  },
1139
1141
  },
1140
1142
  history: {
@@ -29,6 +29,8 @@ export interface AlertSettings {
29
29
  export interface Settings {
30
30
  /** Default exchange when -e flag is omitted */
31
31
  defaultExchange: string;
32
+ /** Active OWS wallet name (used when no --ows or --wallet flag) */
33
+ owsActiveWallet: string;
32
34
  /** Enable referral/builder codes (default: false — opt-in only) */
33
35
  referrals: boolean;
34
36
  /** Per-exchange referral codes (used when referrals=true) */
package/dist/settings.js CHANGED
@@ -4,6 +4,7 @@ const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
4
4
  const SETTINGS_FILE = resolve(PERP_DIR, "settings.json");
5
5
  const DEFAULTS = {
6
6
  defaultExchange: "",
7
+ owsActiveWallet: "",
7
8
  referrals: false,
8
9
  referralCodes: {
9
10
  pacifica: "",
@@ -42,6 +43,7 @@ export function loadSettings() {
42
43
  }
43
44
  return {
44
45
  defaultExchange: stored.defaultExchange ?? DEFAULTS.defaultExchange,
46
+ owsActiveWallet: stored.owsActiveWallet ?? DEFAULTS.owsActiveWallet,
45
47
  referrals: stored.referrals ?? DEFAULTS.referrals,
46
48
  referralCodes: {
47
49
  pacifica: stored.referralCodes?.pacifica ?? DEFAULTS.referralCodes.pacifica,
@@ -1,3 +1,5 @@
1
1
  export type { EvmSigner, SolanaSigner } from "./interface.js";
2
2
  export { LocalEvmSigner } from "./evm-local.js";
3
3
  export { LocalSolanaSigner } from "./solana-local.js";
4
+ export { OwsEvmSigner } from "./ows-evm.js";
5
+ export { OwsSolanaSigner } from "./ows-solana.js";
@@ -1,2 +1,4 @@
1
1
  export { LocalEvmSigner } from "./evm-local.js";
2
2
  export { LocalSolanaSigner } from "./solana-local.js";
3
+ export { OwsEvmSigner } from "./ows-evm.js";
4
+ export { OwsSolanaSigner } from "./ows-solana.js";