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