perp-cli 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +18 -14
- 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 +756 -28
- 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 +84 -34
- package/dist/exchanges/lighter-spot.js +14 -3
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +199 -27
- 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/perp-guardrail.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +255 -360
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +85 -83
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +67 -3
- package/dist/settings.js +1 -2
- package/dist/signer/ows-evm.d.ts +14 -0
- package/dist/signer/ows-evm.js +28 -9
- 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 +2 -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/ows.d.ts +0 -2
- package/dist/commands/ows.js +0 -887
- 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
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
4
|
import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
|
|
6
5
|
import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
|
|
7
6
|
import { loadSettings, saveSettings } from "../settings.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { registerWalletAgentCommands } from "./agent.js";
|
|
8
|
+
import { registerWalletManageCommands } from "./manage.js";
|
|
9
|
+
import { loadOws } from "../signer/ows-loader.js";
|
|
10
|
+
// ── OWS CLI subprocess helper (used by backup/restore/rotate) ──
|
|
11
|
+
//
|
|
12
|
+
// The wallet command tree exposes three different "agent" sub-namespaces;
|
|
13
|
+
// they are intentionally separate because they answer three different
|
|
14
|
+
// questions:
|
|
15
|
+
//
|
|
16
|
+
// * `wallet agent approve <ex> ...` — DEX-side agent registration. Sends
|
|
17
|
+
// EIP-712 approveAgent (or equivalent) to the exchange so the agent
|
|
18
|
+
// wallet can post orders on behalf of the master. State lives partly on
|
|
19
|
+
// the DEX (their auth list) and partly in `~/.perp/settings.json` under
|
|
20
|
+
// `agents.<exchange>.<name>`.
|
|
21
|
+
//
|
|
22
|
+
// * `wallet key create / list / revoke` — OWS-side API key. A vault
|
|
23
|
+
// access token (`ows_key_…`) that lets an off-host agent process invoke
|
|
24
|
+
// OWS signing primitives without holding the master passphrase. Pure
|
|
25
|
+
// OWS construct, no DEX traffic.
|
|
26
|
+
//
|
|
27
|
+
// * `wallet policy create / list / show / delete` — OWS-side guardrail.
|
|
28
|
+
// A policy attached to an OWS API key that bounds what the agent can
|
|
29
|
+
// sign (chain allowlist, per-tx USD cap, daily withdraw cap, etc.).
|
|
30
|
+
// Also pure OWS, evaluated locally before any signature is produced.
|
|
31
|
+
//
|
|
32
|
+
// Future maintainers: do NOT collapse these. They protect different layers.
|
|
33
|
+
async function runOwsCli(args, timeout = 30000) {
|
|
34
|
+
const { resolve: pathResolve } = await import("path");
|
|
35
|
+
const { existsSync: fsExists } = await import("fs");
|
|
36
|
+
const { spawnSync } = await import("child_process");
|
|
37
|
+
const owsBin = [
|
|
38
|
+
pathResolve(process.env.HOME || "~", ".ows", "bin", "ows"),
|
|
39
|
+
"/usr/local/bin/ows",
|
|
40
|
+
].find(p => fsExists(p));
|
|
41
|
+
if (!owsBin) {
|
|
42
|
+
throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
|
|
43
|
+
}
|
|
44
|
+
const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
|
|
45
|
+
const proc = spawnSync(owsBin, args, { encoding: "utf-8", timeout, env: owsEnv });
|
|
46
|
+
if (proc.error)
|
|
47
|
+
throw new Error(`OWS CLI failed: ${proc.error.message}`);
|
|
48
|
+
return { stdout: (proc.stdout || "").trim(), stderr: (proc.stderr || "").trim() };
|
|
11
49
|
}
|
|
12
50
|
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
13
51
|
const WALLETS_FILE = resolve(PERP_DIR, "wallets.json");
|
|
@@ -278,7 +316,7 @@ async function _legacyImport(chain, privateKey, name, isJson) {
|
|
|
278
316
|
}
|
|
279
317
|
}
|
|
280
318
|
// ── Commands ──────────────────────────────────────────────────
|
|
281
|
-
export function registerWalletCommands(program, isJson) {
|
|
319
|
+
export function registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter) {
|
|
282
320
|
const wallet = program.command("wallet").description("Wallet management & on-chain balances");
|
|
283
321
|
// ── generate (OWS — creates multi-chain wallet in encrypted vault) ──
|
|
284
322
|
wallet
|
|
@@ -343,26 +381,123 @@ export function registerWalletCommands(program, isJson) {
|
|
|
343
381
|
}
|
|
344
382
|
});
|
|
345
383
|
// ── import (OWS by default — imports key into encrypted vault) ──
|
|
384
|
+
//
|
|
385
|
+
// Solana key normalization: OWS SDK expects 32-byte hex secret. Phantom
|
|
386
|
+
// exports base58 (88 chars, 64-byte keypair = secret + public). Auto-detect
|
|
387
|
+
// base58/JSON-array/hex and normalize to 32-byte hex secret.
|
|
346
388
|
wallet
|
|
347
|
-
.command("import
|
|
348
|
-
.description("Import a private key into OWS encrypted vault")
|
|
389
|
+
.command("import [privateKey]")
|
|
390
|
+
.description("Import a private key into OWS encrypted vault (use --evm/--solana to import both curves at once)")
|
|
349
391
|
.option("--name <name>", "Wallet alias name", "imported")
|
|
350
|
-
.option("--chain <chain>", "Key type
|
|
392
|
+
.option("--chain <chain>", "Key type for positional <privateKey>: evm (default) or solana", "evm")
|
|
393
|
+
.option("--evm <hex>", "EVM (secp256k1) private key — use with --solana to import both curves")
|
|
394
|
+
.option("--solana <key>", "Solana (ed25519) private key — accepts hex (64), base58 (Phantom export), or JSON byte array. Use with --evm to import both curves")
|
|
351
395
|
.option("--mnemonic", "Import as mnemonic phrase instead of private key")
|
|
396
|
+
.option("--passphrase <pp>", "OWS vault encryption passphrase (default: empty / OWS_PASSPHRASE env)")
|
|
352
397
|
.option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
|
|
353
398
|
.action(async (privateKey, opts) => {
|
|
354
|
-
// Legacy mode
|
|
399
|
+
// Legacy mode (positional only)
|
|
355
400
|
if (opts.legacy) {
|
|
401
|
+
if (!privateKey) {
|
|
402
|
+
const msg = "--legacy mode requires a positional <privateKey>";
|
|
403
|
+
if (isJson()) {
|
|
404
|
+
const { jsonError } = await import("../utils.js");
|
|
405
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
406
|
+
}
|
|
407
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
356
410
|
return _legacyImport(opts.legacy, privateKey, opts.name, isJson);
|
|
357
411
|
}
|
|
412
|
+
// Validation: input mode is exactly one of (positional | --evm/--solana flags | mnemonic)
|
|
413
|
+
const hasPositional = !!privateKey;
|
|
414
|
+
const hasFlagKeys = !!(opts.evm || opts.solana);
|
|
415
|
+
if (!hasPositional && !hasFlagKeys) {
|
|
416
|
+
const msg = "Provide a <privateKey> argument OR --evm/--solana flag(s)";
|
|
417
|
+
if (isJson()) {
|
|
418
|
+
const { jsonError } = await import("../utils.js");
|
|
419
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
420
|
+
}
|
|
421
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
if (hasPositional && hasFlagKeys) {
|
|
425
|
+
const msg = "Cannot mix positional <privateKey> with --evm/--solana flags. Use one input style.";
|
|
426
|
+
if (isJson()) {
|
|
427
|
+
const { jsonError } = await import("../utils.js");
|
|
428
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
429
|
+
}
|
|
430
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
if (opts.mnemonic && hasFlagKeys) {
|
|
434
|
+
const msg = "--mnemonic cannot be combined with --evm/--solana";
|
|
435
|
+
if (isJson()) {
|
|
436
|
+
const { jsonError } = await import("../utils.js");
|
|
437
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
438
|
+
}
|
|
439
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
358
442
|
try {
|
|
359
443
|
const ows = loadOws();
|
|
444
|
+
// Auto-normalize Solana keys: accept hex / base58 (Phantom) / JSON byte array
|
|
445
|
+
const bs58 = (await import("bs58")).default;
|
|
446
|
+
const normalizeSolana = (input) => {
|
|
447
|
+
const s = input.trim();
|
|
448
|
+
// Already hex (with optional 0x prefix)
|
|
449
|
+
const hexNoPrefix = s.startsWith("0x") ? s.slice(2) : s;
|
|
450
|
+
if (/^[0-9a-fA-F]{64}$/.test(hexNoPrefix))
|
|
451
|
+
return hexNoPrefix;
|
|
452
|
+
if (/^[0-9a-fA-F]{128}$/.test(hexNoPrefix)) {
|
|
453
|
+
return hexNoPrefix.slice(0, 64); // 64-byte keypair → first 32 bytes secret
|
|
454
|
+
}
|
|
455
|
+
// JSON byte array
|
|
456
|
+
if (s.startsWith("[")) {
|
|
457
|
+
const arr = JSON.parse(s);
|
|
458
|
+
if (arr.length === 64)
|
|
459
|
+
return Buffer.from(arr.slice(0, 32)).toString("hex");
|
|
460
|
+
if (arr.length === 32)
|
|
461
|
+
return Buffer.from(arr).toString("hex");
|
|
462
|
+
throw new Error(`Invalid Solana JSON byte array length: ${arr.length} (expected 32 or 64)`);
|
|
463
|
+
}
|
|
464
|
+
// base58 (Phantom export — typically 87-88 chars for 64-byte keypair)
|
|
465
|
+
try {
|
|
466
|
+
const decoded = bs58.decode(s);
|
|
467
|
+
if (decoded.length === 64)
|
|
468
|
+
return Buffer.from(decoded.slice(0, 32)).toString("hex");
|
|
469
|
+
if (decoded.length === 32)
|
|
470
|
+
return Buffer.from(decoded).toString("hex");
|
|
471
|
+
throw new Error(`Invalid Solana base58 byte length: ${decoded.length} (expected 32 or 64)`);
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
475
|
+
throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
// Resolve vault passphrase (precedence: --passphrase > OWS_PASSPHRASE env > "")
|
|
479
|
+
const pp = opts.passphrase
|
|
480
|
+
?? (("OWS_PASSPHRASE" in process.env) ? (process.env["OWS_PASSPHRASE"] ?? "") : "");
|
|
360
481
|
let w;
|
|
361
482
|
if (opts.mnemonic) {
|
|
362
|
-
w = ows.importWalletMnemonic(opts.name, privateKey);
|
|
483
|
+
w = ows.importWalletMnemonic(opts.name, privateKey, pp);
|
|
484
|
+
}
|
|
485
|
+
else if (opts.evm && opts.solana) {
|
|
486
|
+
// Dual-curve: both explicit keys (OWS 1.3+ API — both must be present)
|
|
487
|
+
w = ows.importWalletPrivateKey(opts.name, "", // ignored when both secp256k1Key and ed25519Key are provided
|
|
488
|
+
pp, undefined, undefined, opts.evm, normalizeSolana(opts.solana));
|
|
489
|
+
}
|
|
490
|
+
else if (opts.evm) {
|
|
491
|
+
// Single EVM via flag — same as positional + --chain evm
|
|
492
|
+
w = ows.importWalletPrivateKey(opts.name, opts.evm, pp, undefined, "evm");
|
|
493
|
+
}
|
|
494
|
+
else if (opts.solana) {
|
|
495
|
+
// Single Solana via flag — same as positional + --chain solana
|
|
496
|
+
w = ows.importWalletPrivateKey(opts.name, normalizeSolana(opts.solana), pp, undefined, "solana");
|
|
363
497
|
}
|
|
364
498
|
else {
|
|
365
|
-
|
|
499
|
+
const keyArg = opts.chain === "solana" ? normalizeSolana(privateKey) : privateKey;
|
|
500
|
+
w = ows.importWalletPrivateKey(opts.name, keyArg, pp, undefined, opts.chain);
|
|
366
501
|
}
|
|
367
502
|
// Set as active wallet if none set
|
|
368
503
|
const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
|
|
@@ -493,24 +628,21 @@ export function registerWalletCommands(program, isJson) {
|
|
|
493
628
|
await adapter.init();
|
|
494
629
|
const apiKeyIndex = 4;
|
|
495
630
|
const { privateKey: apiKey } = await adapter.setupApiKey(apiKeyIndex);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
631
|
+
// SSOT Rule #3: persist L2 key in encrypted keystore.
|
|
632
|
+
// accountIndex/apiKeyIndex live in settings.agents.lighter and don't
|
|
633
|
+
// need env duplication.
|
|
634
|
+
const { saveLighterKey } = await import("../agent-wallet/lighter-keystore.js");
|
|
635
|
+
saveLighterKey(adapter.accountIndex, apiKeyIndex, apiKey, "");
|
|
499
636
|
lighterApiSetup = { apiKey, accountIndex: adapter.accountIndex };
|
|
500
637
|
}
|
|
501
638
|
catch (e) {
|
|
502
639
|
lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
|
|
503
640
|
}
|
|
504
641
|
}
|
|
505
|
-
const settings = loadSettings();
|
|
506
|
-
const referralHint = !settings.referrals
|
|
507
|
-
? { referrals: false, hint: "Optional: 'perp settings referrals on' to support perp-cli development (no extra fees — exchange rebates only)" }
|
|
508
|
-
: undefined;
|
|
509
642
|
if (isJson())
|
|
510
643
|
return printJson(jsonOk({
|
|
511
644
|
exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default,
|
|
512
645
|
...(resolved === "lighter" && { lighterApiKey: lighterApiSetup }),
|
|
513
|
-
...(referralHint && { referralHint }),
|
|
514
646
|
}));
|
|
515
647
|
console.log(chalk.green(`\n ${resolved} configured.`));
|
|
516
648
|
console.log(` Address: ${chalk.green(address)}`);
|
|
@@ -523,13 +655,9 @@ export function registerWalletCommands(program, isJson) {
|
|
|
523
655
|
}
|
|
524
656
|
else {
|
|
525
657
|
console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
|
|
526
|
-
console.log(chalk.gray(` You can retry: perp
|
|
658
|
+
console.log(chalk.gray(` You can retry: perp wallet agent approve lighter`));
|
|
527
659
|
}
|
|
528
660
|
}
|
|
529
|
-
if (!settings.referrals) {
|
|
530
|
-
console.log(chalk.gray(` Optional: ${chalk.cyan("perp settings referrals on")} to support perp-cli development`));
|
|
531
|
-
console.log(chalk.gray(` (no extra fees — exchange rebates only)`));
|
|
532
|
-
}
|
|
533
661
|
console.log();
|
|
534
662
|
});
|
|
535
663
|
// ── show (show configured exchanges with public addresses) ──
|
|
@@ -554,14 +682,41 @@ export function registerWalletCommands(program, isJson) {
|
|
|
554
682
|
const { valid, address } = await validateKey(entry.chain, entry.key);
|
|
555
683
|
results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
|
|
556
684
|
}
|
|
685
|
+
// SSOT Rule #3: surface OWS active wallet's publicAddresses so agents and
|
|
686
|
+
// commands like `wallet agent verify` can read the master EVM/Solana
|
|
687
|
+
// addresses without relying on env-derived keys.
|
|
688
|
+
const settings = loadSettings();
|
|
689
|
+
const activeOwsName = settings.owsActiveWallet;
|
|
690
|
+
let owsActive = null;
|
|
691
|
+
if (activeOwsName) {
|
|
692
|
+
try {
|
|
693
|
+
const w = loadOws().getWallet(activeOwsName);
|
|
694
|
+
const evm = w.accounts.find(a => a.chainId.startsWith("eip155:"))?.address ?? null;
|
|
695
|
+
const sol = w.accounts.find(a => a.chainId.startsWith("solana:"))?.address ?? null;
|
|
696
|
+
owsActive = { name: activeOwsName, evmAddress: evm, solanaAddress: sol };
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
// OWS unavailable or wallet missing — leave owsActive null. SSOT Rule #2:
|
|
700
|
+
// no fallback to env-derived addresses for the OWS slot.
|
|
701
|
+
owsActive = { name: activeOwsName, evmAddress: null, solanaAddress: null };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
557
704
|
if (isJson()) {
|
|
558
705
|
const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
|
|
559
|
-
return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
|
|
706
|
+
return printJson(jsonOk({ envFile: ENV_FILE, owsActive, exchanges: data }));
|
|
560
707
|
}
|
|
561
708
|
console.log(chalk.cyan.bold("\n Configured Wallets\n"));
|
|
562
|
-
if (
|
|
709
|
+
if (owsActive) {
|
|
710
|
+
console.log(` ${chalk.cyan("OWS Active".padEnd(14))} ${chalk.white.bold(owsActive.name)}`);
|
|
711
|
+
if (owsActive.evmAddress)
|
|
712
|
+
console.log(` ${chalk.cyan(" EVM".padEnd(14))} ${chalk.green(owsActive.evmAddress)}`);
|
|
713
|
+
if (owsActive.solanaAddress)
|
|
714
|
+
console.log(` ${chalk.cyan(" Solana".padEnd(14))} ${chalk.green(owsActive.solanaAddress)}`);
|
|
715
|
+
console.log();
|
|
716
|
+
}
|
|
717
|
+
if (results.length === 0 && !owsActive) {
|
|
563
718
|
console.log(chalk.gray(" No keys configured."));
|
|
564
|
-
console.log(chalk.gray(` Run ${chalk.cyan("perp setup")} or ${chalk.cyan("perp wallet
|
|
719
|
+
console.log(chalk.gray(` Run ${chalk.cyan("perp setup")} or ${chalk.cyan("perp wallet generate")}\n`));
|
|
565
720
|
return;
|
|
566
721
|
}
|
|
567
722
|
for (const { name, address, source } of results) {
|
|
@@ -717,7 +872,33 @@ export function registerWalletCommands(program, isJson) {
|
|
|
717
872
|
if (name) {
|
|
718
873
|
entry = store.wallets[name];
|
|
719
874
|
if (!entry) {
|
|
720
|
-
|
|
875
|
+
// Try OWS vault before failing — balance only needs address + chain type
|
|
876
|
+
try {
|
|
877
|
+
const { loadOws } = await import("../signer/ows-loader.js");
|
|
878
|
+
const ows = loadOws();
|
|
879
|
+
const owsWallet = ows.getWallet(name);
|
|
880
|
+
const evmAccount = owsWallet.accounts.find((a) => a.chainId.startsWith("eip155:"));
|
|
881
|
+
const solAccount = owsWallet.accounts.find((a) => a.chainId.startsWith("solana:"));
|
|
882
|
+
const account = evmAccount ?? solAccount;
|
|
883
|
+
if (account) {
|
|
884
|
+
entry = {
|
|
885
|
+
name,
|
|
886
|
+
type: evmAccount ? "evm" : "solana",
|
|
887
|
+
address: account.address,
|
|
888
|
+
privateKey: "", // OWS wallet — PK stays encrypted in vault
|
|
889
|
+
createdAt: new Date().toISOString(),
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch { /* not in OWS either */ }
|
|
894
|
+
}
|
|
895
|
+
if (!entry) {
|
|
896
|
+
if (isJson()) {
|
|
897
|
+
printJson({ ok: false, error: { code: "KEY_NOT_FOUND", message: `Wallet "${name}" not found in OWS vault or legacy store.` }, meta: { timestamp: new Date().toISOString() } });
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
|
|
901
|
+
}
|
|
721
902
|
process.exit(1);
|
|
722
903
|
}
|
|
723
904
|
}
|
|
@@ -898,4 +1079,551 @@ export function registerWalletCommands(program, isJson) {
|
|
|
898
1079
|
console.log();
|
|
899
1080
|
}
|
|
900
1081
|
});
|
|
1082
|
+
// ── Policy Engine (OWS) ──────────────────────────────────────────────
|
|
1083
|
+
// OWS-side guardrail: rules an agent API key must satisfy at sign time.
|
|
1084
|
+
const policyCmd = wallet.command("policy").description("Policy engine — guardrails for agent signing");
|
|
1085
|
+
policyCmd
|
|
1086
|
+
.command("create")
|
|
1087
|
+
.description("Create a signing policy")
|
|
1088
|
+
.requiredOption("--id <id>", "Unique policy ID")
|
|
1089
|
+
.requiredOption("--name <name>", "Human-readable policy name")
|
|
1090
|
+
.option("--chains <chains>", "Comma-separated allowed CAIP-2 chain IDs")
|
|
1091
|
+
.option("--expires <iso>", "Policy expiry (ISO-8601 timestamp)")
|
|
1092
|
+
.option("--executable <path>", "Path to custom policy executable")
|
|
1093
|
+
.option("--perp-defaults", "Auto-configure with perp-cli guardrail (DEX contracts + spending limits)")
|
|
1094
|
+
.option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
|
|
1095
|
+
.option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
|
|
1096
|
+
.option("--max-withdraw-usd <amount>", "Max per-withdrawal USD", "500")
|
|
1097
|
+
.option("--max-daily-withdraw-usd <amount>", "Max daily withdrawal USD", "2000")
|
|
1098
|
+
.option("--block-approve", "Block approve() and transferFrom() calls")
|
|
1099
|
+
.action(async (opts) => {
|
|
1100
|
+
try {
|
|
1101
|
+
const o = loadOws();
|
|
1102
|
+
const rules = [];
|
|
1103
|
+
if (opts.perpDefaults) {
|
|
1104
|
+
const { ALLOWED_CHAINS } = await import("../guardrail/contracts.js");
|
|
1105
|
+
rules.push({ type: "allowed_chains", chain_ids: ALLOWED_CHAINS });
|
|
1106
|
+
}
|
|
1107
|
+
else if (opts.chains) {
|
|
1108
|
+
rules.push({ type: "allowed_chains", chain_ids: opts.chains.split(",").map(s => s.trim()) });
|
|
1109
|
+
}
|
|
1110
|
+
if (opts.expires) {
|
|
1111
|
+
rules.push({ type: "expires_at", timestamp: opts.expires });
|
|
1112
|
+
}
|
|
1113
|
+
let executablePath = opts.executable;
|
|
1114
|
+
let config;
|
|
1115
|
+
if (opts.perpDefaults) {
|
|
1116
|
+
const { resolve: pathResolve } = await import("path");
|
|
1117
|
+
try {
|
|
1118
|
+
const { createRequire: cr } = await import("node:module");
|
|
1119
|
+
const req = cr(import.meta.url);
|
|
1120
|
+
executablePath = pathResolve(req.resolve("perp-cli/dist/guardrail/perp-guardrail.js"));
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
executablePath = pathResolve(process.cwd(), "node_modules/.bin/perp-guardrail");
|
|
1124
|
+
}
|
|
1125
|
+
config = {
|
|
1126
|
+
max_tx_usd: parseInt(opts.maxTxUsd || "1000"),
|
|
1127
|
+
max_daily_usd: parseInt(opts.maxDailyUsd || "5000"),
|
|
1128
|
+
max_withdraw_usd: parseInt(opts.maxWithdrawUsd || "500"),
|
|
1129
|
+
max_daily_withdraw_usd: parseInt(opts.maxDailyWithdrawUsd || "2000"),
|
|
1130
|
+
...(opts.blockApprove ? { block_selectors: ["0x095ea7b3", "0x23b872dd"] } : {}),
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
const policyDef = {
|
|
1134
|
+
id: opts.id, name: opts.name, version: 1, created_at: new Date().toISOString(),
|
|
1135
|
+
rules, executable: executablePath || null, config: config || null, action: "deny",
|
|
1136
|
+
};
|
|
1137
|
+
o.createPolicy(JSON.stringify(policyDef));
|
|
1138
|
+
if (isJson())
|
|
1139
|
+
return printJson(jsonOk(policyDef));
|
|
1140
|
+
console.log(chalk.green(`\n Policy "${opts.name}" created (${opts.id})`));
|
|
1141
|
+
if (opts.chains || opts.perpDefaults) {
|
|
1142
|
+
const chainRule = rules.find(r => r.type === "allowed_chains");
|
|
1143
|
+
if (chainRule)
|
|
1144
|
+
console.log(` Chains: ${chalk.cyan(chainRule.chain_ids.length + " chains")}`);
|
|
1145
|
+
}
|
|
1146
|
+
if (opts.expires)
|
|
1147
|
+
console.log(` Expires: ${chalk.yellow(opts.expires)}`);
|
|
1148
|
+
if (executablePath)
|
|
1149
|
+
console.log(` Executable: ${chalk.gray(executablePath)}`);
|
|
1150
|
+
if (config)
|
|
1151
|
+
console.log(` Limits: ${chalk.cyan(`$${config.max_tx_usd}/tx, $${config.max_daily_usd}/day`)}`);
|
|
1152
|
+
console.log();
|
|
1153
|
+
}
|
|
1154
|
+
catch (e) {
|
|
1155
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1156
|
+
if (isJson()) {
|
|
1157
|
+
const { jsonError } = await import("../utils.js");
|
|
1158
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1159
|
+
}
|
|
1160
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
policyCmd
|
|
1165
|
+
.command("list")
|
|
1166
|
+
.description("List all policies")
|
|
1167
|
+
.action(async () => {
|
|
1168
|
+
try {
|
|
1169
|
+
const o = loadOws();
|
|
1170
|
+
const policies = o.listPolicies();
|
|
1171
|
+
if (isJson())
|
|
1172
|
+
return printJson(jsonOk({ policies }));
|
|
1173
|
+
if (policies.length === 0) {
|
|
1174
|
+
console.log(chalk.gray("\n No policies defined.\n"));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
console.log(chalk.cyan.bold("\n OWS Policies\n"));
|
|
1178
|
+
const rows = policies.map(p => {
|
|
1179
|
+
const ruleTypes = p.rules.map(r => r.type).join(", ");
|
|
1180
|
+
return [chalk.white.bold(p.id), p.name, chalk.cyan(ruleTypes), chalk.gray(p.created_at?.split("T")[0] ?? "-")];
|
|
1181
|
+
});
|
|
1182
|
+
console.log(makeTable(["ID", "Name", "Rules", "Created"], rows));
|
|
1183
|
+
console.log();
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1187
|
+
if (isJson()) {
|
|
1188
|
+
const { jsonError } = await import("../utils.js");
|
|
1189
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1190
|
+
}
|
|
1191
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
policyCmd
|
|
1196
|
+
.command("show <id>")
|
|
1197
|
+
.description("Show policy details")
|
|
1198
|
+
.action(async (id) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const o = loadOws();
|
|
1201
|
+
const p = o.getPolicy(id);
|
|
1202
|
+
if (isJson())
|
|
1203
|
+
return printJson(jsonOk(p));
|
|
1204
|
+
console.log(chalk.cyan.bold(`\n Policy: ${id}\n`));
|
|
1205
|
+
console.log(JSON.stringify(p, null, 2));
|
|
1206
|
+
console.log();
|
|
1207
|
+
}
|
|
1208
|
+
catch (e) {
|
|
1209
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1210
|
+
if (isJson()) {
|
|
1211
|
+
const { jsonError } = await import("../utils.js");
|
|
1212
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1213
|
+
}
|
|
1214
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
policyCmd
|
|
1219
|
+
.command("delete <id>")
|
|
1220
|
+
.description("Delete a policy")
|
|
1221
|
+
.action(async (id) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const o = loadOws();
|
|
1224
|
+
o.deletePolicy(id);
|
|
1225
|
+
if (isJson())
|
|
1226
|
+
return printJson(jsonOk({ deleted: id }));
|
|
1227
|
+
console.log(chalk.yellow(`\n Policy "${id}" deleted.\n`));
|
|
1228
|
+
}
|
|
1229
|
+
catch (e) {
|
|
1230
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1231
|
+
if (isJson()) {
|
|
1232
|
+
const { jsonError } = await import("../utils.js");
|
|
1233
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1234
|
+
}
|
|
1235
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1236
|
+
process.exit(1);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
// ── API Keys (OWS) ──────────────────────────────────────────────────
|
|
1240
|
+
// OWS-side scoped agent access tokens. NOT to be confused with
|
|
1241
|
+
// `wallet agent` (DEX-side delegation) — see comment near runOwsCli.
|
|
1242
|
+
const keyCmd = wallet.command("key").description("API key management — scoped agent access tokens");
|
|
1243
|
+
keyCmd
|
|
1244
|
+
.command("create")
|
|
1245
|
+
.description("Create an API key for policy-gated agent signing")
|
|
1246
|
+
.requiredOption("--name <name>", "Key name (e.g. trading-bot)")
|
|
1247
|
+
.requiredOption("--wallets <names>", "Comma-separated OWS wallet names")
|
|
1248
|
+
.option("--policy <ids>", "Comma-separated policy IDs to enforce")
|
|
1249
|
+
.option("--expires <iso>", "Key expiry (ISO-8601)")
|
|
1250
|
+
.option("-p, --passphrase <pass>", "Vault passphrase", "")
|
|
1251
|
+
.action(async (opts) => {
|
|
1252
|
+
try {
|
|
1253
|
+
const o = loadOws();
|
|
1254
|
+
const walletIds = opts.wallets.split(",").map(s => s.trim());
|
|
1255
|
+
const policyIds = opts.policy ? opts.policy.split(",").map(s => s.trim()) : [];
|
|
1256
|
+
const result = o.createApiKey(opts.name, walletIds, policyIds, opts.passphrase, opts.expires);
|
|
1257
|
+
if (isJson())
|
|
1258
|
+
return printJson(jsonOk({ id: result.id, name: result.name, token: result.token, wallets: walletIds, policies: policyIds, expires: opts.expires ?? null }));
|
|
1259
|
+
console.log(chalk.green.bold("\n OWS API Key Created\n"));
|
|
1260
|
+
console.log(` Name: ${chalk.white.bold(result.name)}`);
|
|
1261
|
+
console.log(` ID: ${chalk.gray(result.id)}`);
|
|
1262
|
+
console.log(` Wallets: ${chalk.cyan(walletIds.join(", "))}`);
|
|
1263
|
+
if (policyIds.length)
|
|
1264
|
+
console.log(` Policies: ${chalk.cyan(policyIds.join(", "))}`);
|
|
1265
|
+
if (opts.expires)
|
|
1266
|
+
console.log(` Expires: ${chalk.yellow(opts.expires)}`);
|
|
1267
|
+
console.log();
|
|
1268
|
+
console.log(` Token: ${chalk.yellow.bold(result.token)}`);
|
|
1269
|
+
console.log(chalk.red("\n Save this token — it will not be shown again!\n"));
|
|
1270
|
+
console.log(chalk.gray(" Usage: perp --ows <wallet> --ows-key <token> -e hl trade ..."));
|
|
1271
|
+
console.log(chalk.gray(" or: OWS_API_KEY=<token> perp --ows <wallet> -e hl trade ...\n"));
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1275
|
+
if (isJson()) {
|
|
1276
|
+
const { jsonError } = await import("../utils.js");
|
|
1277
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1278
|
+
}
|
|
1279
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
keyCmd
|
|
1284
|
+
.command("list")
|
|
1285
|
+
.description("List all API keys (tokens are never displayed)")
|
|
1286
|
+
.action(async () => {
|
|
1287
|
+
try {
|
|
1288
|
+
const o = loadOws();
|
|
1289
|
+
const keys = o.listApiKeys();
|
|
1290
|
+
if (isJson())
|
|
1291
|
+
return printJson(jsonOk({ keys }));
|
|
1292
|
+
if (keys.length === 0) {
|
|
1293
|
+
console.log(chalk.gray("\n No API keys found.\n"));
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
console.log(chalk.cyan.bold("\n OWS API Keys\n"));
|
|
1297
|
+
const rows = keys.map(k => [
|
|
1298
|
+
chalk.white.bold(k.name), chalk.gray(k.id), chalk.gray(k.created_at?.split("T")[0] ?? "-"),
|
|
1299
|
+
]);
|
|
1300
|
+
console.log(makeTable(["Name", "ID", "Created"], rows));
|
|
1301
|
+
console.log();
|
|
1302
|
+
}
|
|
1303
|
+
catch (e) {
|
|
1304
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1305
|
+
if (isJson()) {
|
|
1306
|
+
const { jsonError } = await import("../utils.js");
|
|
1307
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1308
|
+
}
|
|
1309
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
keyCmd
|
|
1314
|
+
.command("revoke <id>")
|
|
1315
|
+
.description("Revoke an API key (token becomes permanently invalid)")
|
|
1316
|
+
.action(async (id) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const o = loadOws();
|
|
1319
|
+
o.revokeApiKey(id);
|
|
1320
|
+
if (isJson())
|
|
1321
|
+
return printJson(jsonOk({ revoked: id }));
|
|
1322
|
+
console.log(chalk.yellow(`\n API key "${id}" revoked.\n`));
|
|
1323
|
+
}
|
|
1324
|
+
catch (e) {
|
|
1325
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1326
|
+
if (isJson()) {
|
|
1327
|
+
const { jsonError } = await import("../utils.js");
|
|
1328
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1329
|
+
}
|
|
1330
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
// ── deposit (OWS-backed MoonPay on-ramp) ─────────────────────────────
|
|
1335
|
+
wallet
|
|
1336
|
+
.command("deposit <walletName>")
|
|
1337
|
+
.description("Create multi-chain deposit via MoonPay (auto-converts to USDC)")
|
|
1338
|
+
.option("-c, --chain <chain>", "Target chain for USDC delivery")
|
|
1339
|
+
.option("--for <exchange>", "Target exchange (auto-selects chain: hl→arbitrum, pac→solana, lt→ethereum)")
|
|
1340
|
+
.action(async (walletName, opts) => {
|
|
1341
|
+
try {
|
|
1342
|
+
// Resolve chain from exchange if specified
|
|
1343
|
+
const exchangeChainMap = {
|
|
1344
|
+
hyperliquid: "arbitrum", hl: "arbitrum",
|
|
1345
|
+
pacifica: "solana", pac: "solana",
|
|
1346
|
+
lighter: "ethereum", lt: "ethereum",
|
|
1347
|
+
};
|
|
1348
|
+
const forExchange = opts.for;
|
|
1349
|
+
const chain = opts.chain || (forExchange ? exchangeChainMap[forExchange.toLowerCase()] : undefined) || "base";
|
|
1350
|
+
// Verify wallet exists first
|
|
1351
|
+
const o = loadOws();
|
|
1352
|
+
o.getWallet(walletName);
|
|
1353
|
+
// Find OWS CLI binary
|
|
1354
|
+
const { resolve: pathResolve } = await import("path");
|
|
1355
|
+
const { existsSync: fsExists } = await import("fs");
|
|
1356
|
+
const owsBin = [
|
|
1357
|
+
pathResolve(process.env.HOME || "~", ".ows", "bin", "ows"),
|
|
1358
|
+
"/usr/local/bin/ows",
|
|
1359
|
+
].find(p => fsExists(p));
|
|
1360
|
+
if (!owsBin) {
|
|
1361
|
+
throw new Error("OWS CLI not found. Install: curl -fsSL https://docs.openwallet.sh/install.sh | bash");
|
|
1362
|
+
}
|
|
1363
|
+
// Call OWS CLI subprocess for native MoonPay deposit
|
|
1364
|
+
// OWS CLI writes addresses to stderr, URL to stdout
|
|
1365
|
+
const owsEnv = { ...process.env, PATH: `${pathResolve(process.env.HOME || "~", ".ows", "bin")}:${process.env.PATH}` };
|
|
1366
|
+
try {
|
|
1367
|
+
const { spawnSync } = await import("child_process");
|
|
1368
|
+
const proc = spawnSync(owsBin, ["fund", "deposit", "--wallet", walletName, "--chain", chain], {
|
|
1369
|
+
encoding: "utf-8", timeout: 30000, env: owsEnv,
|
|
1370
|
+
});
|
|
1371
|
+
const stderrOutput = proc.stderr || "";
|
|
1372
|
+
const combined = stderrOutput + "\n" + (proc.stdout || "");
|
|
1373
|
+
// Parse deposit addresses
|
|
1374
|
+
const addresses = {};
|
|
1375
|
+
for (const line of combined.split("\n")) {
|
|
1376
|
+
const match = line.match(/^\s+(bitcoin|ethereum|solana|tron)\s+(\S+)/);
|
|
1377
|
+
if (match)
|
|
1378
|
+
addresses[match[1]] = match[2];
|
|
1379
|
+
}
|
|
1380
|
+
const urlMatch = combined.match(/(https:\/\/moonpay\S+)/);
|
|
1381
|
+
const depositUrl = urlMatch?.[1] ?? null;
|
|
1382
|
+
// Find our wallet's target address
|
|
1383
|
+
const w = o.getWallet(walletName);
|
|
1384
|
+
const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "";
|
|
1385
|
+
if (isJson()) {
|
|
1386
|
+
return printJson(jsonOk({
|
|
1387
|
+
wallet: walletName,
|
|
1388
|
+
chain,
|
|
1389
|
+
exchange: forExchange || null,
|
|
1390
|
+
targetAddress: evmAddr,
|
|
1391
|
+
depositAddresses: addresses,
|
|
1392
|
+
depositUrl,
|
|
1393
|
+
note: `Send any crypto to the deposit addresses. Funds auto-convert to USDC and deliver to ${evmAddr} on ${chain}.`,
|
|
1394
|
+
}));
|
|
1395
|
+
}
|
|
1396
|
+
// Pass through the human-readable output
|
|
1397
|
+
if (stderrOutput.trim()) {
|
|
1398
|
+
console.log();
|
|
1399
|
+
console.log(stderrOutput.trim());
|
|
1400
|
+
}
|
|
1401
|
+
if (depositUrl)
|
|
1402
|
+
console.log(`\n${depositUrl}`);
|
|
1403
|
+
console.log();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
catch (spawnErr) {
|
|
1407
|
+
throw new Error(`OWS CLI deposit failed: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
catch (e) {
|
|
1411
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1412
|
+
if (isJson()) {
|
|
1413
|
+
const { jsonError } = await import("../utils.js");
|
|
1414
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1415
|
+
}
|
|
1416
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
// ── Setup Wizard (one-click vault + policy + agent key) ──────────────
|
|
1421
|
+
wallet
|
|
1422
|
+
.command("setup")
|
|
1423
|
+
.description("One-click setup: create wallet + guardrail policy + agent API key")
|
|
1424
|
+
.option("-n, --name <name>", "Wallet name", "perp-trading")
|
|
1425
|
+
.option("--max-tx-usd <amount>", "Max per-transaction USD", "1000")
|
|
1426
|
+
.option("--max-daily-usd <amount>", "Max daily spending USD", "5000")
|
|
1427
|
+
.option("--skip-key", "Skip API key creation")
|
|
1428
|
+
.action(async (opts) => {
|
|
1429
|
+
try {
|
|
1430
|
+
const o = loadOws();
|
|
1431
|
+
// Step 1: Create or reuse wallet
|
|
1432
|
+
let w;
|
|
1433
|
+
let walletCreated = false;
|
|
1434
|
+
try {
|
|
1435
|
+
w = o.getWallet(opts.name);
|
|
1436
|
+
if (!isJson())
|
|
1437
|
+
console.log(chalk.gray(`\n Wallet "${opts.name}" already exists. Using it.`));
|
|
1438
|
+
}
|
|
1439
|
+
catch {
|
|
1440
|
+
w = o.createWallet(opts.name, "", 12);
|
|
1441
|
+
walletCreated = true;
|
|
1442
|
+
const settings = loadSettings();
|
|
1443
|
+
if (!settings.owsActiveWallet) {
|
|
1444
|
+
settings.owsActiveWallet = opts.name;
|
|
1445
|
+
saveSettings(settings);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const evmAddr = w.accounts.find((a) => a.chainId.startsWith("eip155:"))?.address ?? "-";
|
|
1449
|
+
const solAddr = w.accounts.find((a) => a.chainId.startsWith("solana:"))?.address ?? "-";
|
|
1450
|
+
if (!isJson() && walletCreated) {
|
|
1451
|
+
console.log(chalk.green.bold(`\n 1. Wallet Created: ${opts.name}`));
|
|
1452
|
+
console.log(` EVM: ${chalk.green(evmAddr)}`);
|
|
1453
|
+
console.log(` Solana: ${chalk.green(solAddr)}`);
|
|
1454
|
+
}
|
|
1455
|
+
// Step 2: Create guardrail policy
|
|
1456
|
+
const policyId = `perp-guardrail-${opts.name}`;
|
|
1457
|
+
const { ALLOWED_CHAINS } = await import("../guardrail/contracts.js");
|
|
1458
|
+
const { resolve: pathResolve } = await import("path");
|
|
1459
|
+
let executablePath;
|
|
1460
|
+
try {
|
|
1461
|
+
const { createRequire: cr } = await import("node:module");
|
|
1462
|
+
const req = cr(import.meta.url);
|
|
1463
|
+
executablePath = pathResolve(req.resolve("perp-cli/dist/guardrail/perp-guardrail.js"));
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
executablePath = pathResolve(process.cwd(), "node_modules/.bin/perp-guardrail");
|
|
1467
|
+
}
|
|
1468
|
+
const policyDef = {
|
|
1469
|
+
id: policyId, name: `perp-cli guardrail (${opts.name})`, version: 1,
|
|
1470
|
+
created_at: new Date().toISOString(),
|
|
1471
|
+
rules: [{ type: "allowed_chains", chain_ids: ALLOWED_CHAINS }],
|
|
1472
|
+
executable: executablePath,
|
|
1473
|
+
config: {
|
|
1474
|
+
max_tx_usd: parseInt(opts.maxTxUsd),
|
|
1475
|
+
max_daily_usd: parseInt(opts.maxDailyUsd),
|
|
1476
|
+
max_withdraw_usd: Math.round(parseInt(opts.maxTxUsd) / 2),
|
|
1477
|
+
max_daily_withdraw_usd: Math.round(parseInt(opts.maxDailyUsd) / 2.5),
|
|
1478
|
+
block_selectors: ["0x095ea7b3", "0x23b872dd"],
|
|
1479
|
+
},
|
|
1480
|
+
action: "deny",
|
|
1481
|
+
};
|
|
1482
|
+
try {
|
|
1483
|
+
o.deletePolicy(policyId);
|
|
1484
|
+
}
|
|
1485
|
+
catch { /* doesn't exist yet */ }
|
|
1486
|
+
o.createPolicy(JSON.stringify(policyDef));
|
|
1487
|
+
if (!isJson()) {
|
|
1488
|
+
console.log(chalk.green.bold(`\n 2. Guardrail Policy Created: ${policyId}`));
|
|
1489
|
+
console.log(` Limits: ${chalk.cyan(`$${opts.maxTxUsd}/tx, $${opts.maxDailyUsd}/day`)}`);
|
|
1490
|
+
console.log(` Chains: ${chalk.cyan(`${ALLOWED_CHAINS.length} chains (all perp-cli exchanges)`)}`);
|
|
1491
|
+
}
|
|
1492
|
+
// Step 3: Create API key
|
|
1493
|
+
let token;
|
|
1494
|
+
let keyId;
|
|
1495
|
+
if (!opts.skipKey) {
|
|
1496
|
+
const keyName = `${opts.name}-agent`;
|
|
1497
|
+
const result = o.createApiKey(keyName, [opts.name], [policyId], "", undefined);
|
|
1498
|
+
token = result.token;
|
|
1499
|
+
keyId = result.id;
|
|
1500
|
+
if (!isJson()) {
|
|
1501
|
+
console.log(chalk.green.bold(`\n 3. Agent API Key Created: ${keyName}`));
|
|
1502
|
+
console.log(` ID: ${chalk.gray(keyId)}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (isJson()) {
|
|
1506
|
+
return printJson(jsonOk({
|
|
1507
|
+
wallet: { name: opts.name, evm: evmAddr, solana: solAddr, created: walletCreated },
|
|
1508
|
+
policy: { id: policyId, maxTxUsd: parseInt(opts.maxTxUsd), maxDailyUsd: parseInt(opts.maxDailyUsd) },
|
|
1509
|
+
apiKey: token ? { id: keyId, token } : null,
|
|
1510
|
+
}));
|
|
1511
|
+
}
|
|
1512
|
+
console.log(chalk.cyan.bold("\n ─── Setup Complete ───\n"));
|
|
1513
|
+
if (token) {
|
|
1514
|
+
console.log(` ${chalk.yellow.bold("Agent Token:")} ${chalk.yellow(token)}`);
|
|
1515
|
+
console.log(chalk.red(" Save this token — it will not be shown again!\n"));
|
|
1516
|
+
console.log(chalk.white.bold(" Usage:\n"));
|
|
1517
|
+
console.log(chalk.gray(" # Owner mode (no policy limits):"));
|
|
1518
|
+
console.log(` ${chalk.cyan(`perp --ows ${opts.name} -e hl trade buy ETH 0.1`)}\n`);
|
|
1519
|
+
console.log(chalk.gray(" # Agent mode (policy enforced):"));
|
|
1520
|
+
console.log(` ${chalk.cyan(`perp --ows ${opts.name} --ows-key ${token.slice(0, 16)}... -e hl trade buy ETH 0.1`)}\n`);
|
|
1521
|
+
}
|
|
1522
|
+
console.log(chalk.gray(" Fund your wallet before trading:"));
|
|
1523
|
+
console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for hl`)} ${chalk.gray("(Hyperliquid → Arbitrum USDC)")}`);
|
|
1524
|
+
console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for pac`)} ${chalk.gray("(Pacifica → Solana USDC)")}`);
|
|
1525
|
+
console.log(` ${chalk.cyan(`perp wallet deposit ${opts.name} --for lt`)} ${chalk.gray("(Lighter → Ethereum USDC)")}\n`);
|
|
1526
|
+
}
|
|
1527
|
+
catch (e) {
|
|
1528
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1529
|
+
if (isJson()) {
|
|
1530
|
+
const { jsonError } = await import("../utils.js");
|
|
1531
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1532
|
+
}
|
|
1533
|
+
console.error(chalk.red(`\n OWS setup error: ${msg}\n`));
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
// ── Backup / Restore (encrypted vault archive) ────────────────────────
|
|
1538
|
+
wallet
|
|
1539
|
+
.command("backup")
|
|
1540
|
+
.description("Backup entire OWS vault (encrypted archive)")
|
|
1541
|
+
.option("-o, --output <path>", "Output file path", `ows-backup-${new Date().toISOString().split("T")[0]}.tar.gz.enc`)
|
|
1542
|
+
.action(async (opts) => {
|
|
1543
|
+
try {
|
|
1544
|
+
const { stdout, stderr } = await runOwsCli(["backup", "--output", opts.output]);
|
|
1545
|
+
if (isJson())
|
|
1546
|
+
return printJson(jsonOk({ backup: opts.output, output: stderr || stdout }));
|
|
1547
|
+
if (stderr)
|
|
1548
|
+
console.log("\n" + stderr);
|
|
1549
|
+
if (stdout)
|
|
1550
|
+
console.log(stdout);
|
|
1551
|
+
console.log();
|
|
1552
|
+
}
|
|
1553
|
+
catch (e) {
|
|
1554
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1555
|
+
if (isJson()) {
|
|
1556
|
+
const { jsonError } = await import("../utils.js");
|
|
1557
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1558
|
+
}
|
|
1559
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
wallet
|
|
1564
|
+
.command("restore <file>")
|
|
1565
|
+
.description("Restore OWS vault from encrypted backup")
|
|
1566
|
+
.action(async (file) => {
|
|
1567
|
+
try {
|
|
1568
|
+
const { stdout, stderr } = await runOwsCli(["restore", "--input", file]);
|
|
1569
|
+
if (isJson())
|
|
1570
|
+
return printJson(jsonOk({ restored: file, output: stderr || stdout }));
|
|
1571
|
+
if (stderr)
|
|
1572
|
+
console.log("\n" + stderr);
|
|
1573
|
+
if (stdout)
|
|
1574
|
+
console.log(stdout);
|
|
1575
|
+
console.log();
|
|
1576
|
+
}
|
|
1577
|
+
catch (e) {
|
|
1578
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1579
|
+
if (isJson()) {
|
|
1580
|
+
const { jsonError } = await import("../utils.js");
|
|
1581
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1582
|
+
}
|
|
1583
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
// ── Key rotation (create new wallet + transfer assets) ───────────────
|
|
1588
|
+
wallet
|
|
1589
|
+
.command("rotate")
|
|
1590
|
+
.description("Rotate wallet keys (create new wallet + transfer assets)")
|
|
1591
|
+
.requiredOption("--from <name>", "Source wallet name")
|
|
1592
|
+
.requiredOption("--to <name>", "Target wallet name (created if not exists)")
|
|
1593
|
+
.option("--chain <chain>", "Chain to rotate on", "evm")
|
|
1594
|
+
.action(async (opts) => {
|
|
1595
|
+
try {
|
|
1596
|
+
const chainMap = { evm: "eip155:1", arbitrum: "eip155:42161", base: "eip155:8453", solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" };
|
|
1597
|
+
const chainArg = chainMap[opts.chain] || opts.chain;
|
|
1598
|
+
const { stdout, stderr } = await runOwsCli(["wallet", "rotate", "--from", opts.from, "--to", opts.to, "--chain", chainArg], 120000);
|
|
1599
|
+
if (isJson())
|
|
1600
|
+
return printJson(jsonOk({ from: opts.from, to: opts.to, chain: chainArg, output: stderr || stdout }));
|
|
1601
|
+
if (stderr)
|
|
1602
|
+
console.log("\n" + stderr);
|
|
1603
|
+
if (stdout)
|
|
1604
|
+
console.log(stdout);
|
|
1605
|
+
console.log();
|
|
1606
|
+
}
|
|
1607
|
+
catch (e) {
|
|
1608
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1609
|
+
if (isJson()) {
|
|
1610
|
+
const { jsonError } = await import("../utils.js");
|
|
1611
|
+
return printJson(jsonError("OWS_ERROR", msg));
|
|
1612
|
+
}
|
|
1613
|
+
console.error(chalk.red(`\n OWS error: ${msg}\n`));
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
// ── Agent (DEX-side delegation) ──────────────────────────────────────
|
|
1618
|
+
// NOTE: Distinct from `wallet key` (OWS API token) and `wallet policy`
|
|
1619
|
+
// (OWS guardrail). See runOwsCli comment above for the three-way split.
|
|
1620
|
+
registerWalletAgentCommands(wallet, isJson);
|
|
1621
|
+
// ── Manage (per-DEX exchange settings — Pacifica + Lighter) ──────────
|
|
1622
|
+
// Absorbed from former top-level `perp manage`. Holds exchange-specific
|
|
1623
|
+
// settings: margin mode, subaccount, lake, builder/referral, API keys.
|
|
1624
|
+
// Skipped when adapter wiring isn't supplied (test programs may register
|
|
1625
|
+
// wallet without adapters).
|
|
1626
|
+
if (getAdapter && getPacificaAdapter) {
|
|
1627
|
+
registerWalletManageCommands(wallet, getAdapter, isJson, getPacificaAdapter);
|
|
1628
|
+
}
|
|
901
1629
|
}
|