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/index.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// MUST be first import — installs a stderr filter for --json mode so SDK
|
|
3
|
+
// startup banners don't leak into machine output. ESM hoists all static
|
|
4
|
+
// imports to the top, but executes them in source order, so this side-effect
|
|
5
|
+
// import runs before any DEX SDK is loaded.
|
|
6
|
+
import "./_stderr-filter.js";
|
|
2
7
|
// Save original process.exit before any SDK (Go WASM) can patch it
|
|
3
8
|
const _origExit = process.exit.bind(process);
|
|
4
9
|
import { config } from "dotenv";
|
|
5
10
|
import { resolve } from "path";
|
|
6
11
|
import { createRequire } from "node:module";
|
|
7
|
-
//
|
|
12
|
+
// SSOT Rule #3: load only ~/.perp/.env. The CWD .env auto-load was removed
|
|
13
|
+
// because it caused master-address pollution when run from a project tree
|
|
14
|
+
// containing leftover dev keys (`wallet show` returned env-derived addresses
|
|
15
|
+
// instead of the OWS active wallet's). If you need ad-hoc env overrides,
|
|
16
|
+
// export them in your shell — keystore is the canonical source.
|
|
8
17
|
config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
|
|
9
|
-
config();
|
|
10
18
|
import { Command } from "commander";
|
|
11
19
|
import chalk from "chalk";
|
|
12
20
|
import { Keypair } from "@solana/web3.js";
|
|
@@ -17,28 +25,26 @@ import { resolveExchangeName } from "./exchanges/registry.js";
|
|
|
17
25
|
import { registerMarketCommands } from "./commands/market.js";
|
|
18
26
|
import { registerAccountCommands } from "./commands/account.js";
|
|
19
27
|
import { registerTradeCommands } from "./commands/trade.js";
|
|
20
|
-
|
|
28
|
+
// manage commands now register under `wallet manage ...` via registerWalletCommands.
|
|
21
29
|
// stream commands removed — WS feeds still used by dashboard/event-stream internally
|
|
22
30
|
import { registerArbCommands } from "./commands/arb.js";
|
|
23
31
|
import { registerWalletCommands } from "./commands/wallet.js";
|
|
24
|
-
|
|
25
|
-
import { registerBridgeCommands } from "./commands/bridge.js";
|
|
26
|
-
// deposit + withdraw merged into funds
|
|
32
|
+
// bridge + rebalance now nested under `funds` — registered inside registerFundsCommands.
|
|
27
33
|
import { registerFundsCommands } from "./commands/funds.js";
|
|
28
34
|
// alert commands removed
|
|
29
35
|
import { registerArbAutoCommands } from "./commands/arb-auto.js";
|
|
30
36
|
import { registerArbManageCommands } from "./commands/arb/index.js";
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
import { registerBotCommands } from "./commands/bot.js";
|
|
37
|
+
// Agent commands now register under `wallet agent ...` via registerWalletCommands.
|
|
38
|
+
import { registerStrategyCommands } from "./commands/bot.js";
|
|
34
39
|
import { registerRiskCommands } from "./commands/risk.js";
|
|
35
40
|
import { registerHistoryCommands } from "./commands/history.js";
|
|
36
41
|
import { registerSettingsCommands } from "./commands/settings.js";
|
|
37
42
|
// dex commands merged into market (hip3) — use --dex flag for markets/balance
|
|
38
|
-
|
|
43
|
+
// `plan` is no longer top-level — registered as `strategy plan` inside
|
|
44
|
+
// registerStrategyCommands.
|
|
39
45
|
// funding merged into arb.ts
|
|
40
46
|
import { registerBacktestCommands } from "./commands/backtest.js";
|
|
41
|
-
import {
|
|
47
|
+
import { registerPortfolioCommand } from "./commands/portfolio.js";
|
|
42
48
|
import { registerInitCommand } from "./commands/init.js";
|
|
43
49
|
import { registerAlertCommands } from "./commands/alerts.js";
|
|
44
50
|
import { loadSettings, saveSettings } from "./settings.js";
|
|
@@ -63,6 +69,8 @@ program
|
|
|
63
69
|
.option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
|
|
64
70
|
.option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
|
|
65
71
|
.option("--ows-key <token>", "OWS API key token (ows_key_...) for policy-gated agent access")
|
|
72
|
+
.option("--no-agent", "Bypass agent wallet routing for this run; falls back to OWS master or PK direct")
|
|
73
|
+
.option("--passphrase <pp>", "Master OWS passphrase (also accepts OWS_PASSPHRASE env or stdin)")
|
|
66
74
|
.option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
|
|
67
75
|
.configureOutput({
|
|
68
76
|
writeErr: (str) => {
|
|
@@ -96,6 +104,9 @@ async function getAdapter() {
|
|
|
96
104
|
const owsName = opts.ows;
|
|
97
105
|
const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
|
|
98
106
|
const walletName = owsName ? `ows:${owsName}` : opts.wallet;
|
|
107
|
+
// Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
|
|
108
|
+
// Normalize so downstream wiring can use a single boolean consistently.
|
|
109
|
+
const noAgent = opts.agent === false;
|
|
99
110
|
// Skip cache when --wallet/--ows is specified (different wallet = different account)
|
|
100
111
|
if (!walletName && _adapter)
|
|
101
112
|
return _adapter;
|
|
@@ -115,6 +126,27 @@ async function getAdapter() {
|
|
|
115
126
|
const settings = loadSettings();
|
|
116
127
|
const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
|
|
117
128
|
_pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
|
|
129
|
+
// Tier 2: OWS master if --ows or settings.owsActiveWallet (Phase 2c)
|
|
130
|
+
const pacOwsName = owsName || _settings.owsActiveWallet;
|
|
131
|
+
if (pacOwsName && !pk) {
|
|
132
|
+
const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
|
|
133
|
+
const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
|
|
134
|
+
const pp = await resolvePassphrase({ flag: opts.passphrase });
|
|
135
|
+
if (pp !== null)
|
|
136
|
+
_pacificaAdapter.setSigner(OwsSolanaSigner.create(pacOwsName, pp));
|
|
137
|
+
}
|
|
138
|
+
// Tier 1: agent if registered (Phase 2c)
|
|
139
|
+
const { getAgent: getPacAgent } = await import("./agent-wallet/store.js");
|
|
140
|
+
const pacAgentMeta = getPacAgent("pacifica");
|
|
141
|
+
if (pacAgentMeta) {
|
|
142
|
+
const { OwsSolanaSigner: PacOwsSolanaSigner } = await import("./signer/ows-solana.js");
|
|
143
|
+
// Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
|
|
144
|
+
// open with "" to match. Agent at-rest encryption handled by OWS storage layer.
|
|
145
|
+
const agentSigner = PacOwsSolanaSigner.create(pacAgentMeta.agentWalletName, "");
|
|
146
|
+
_pacificaAdapter.setAgentSigner(pacAgentMeta, agentSigner);
|
|
147
|
+
}
|
|
148
|
+
if (noAgent)
|
|
149
|
+
_pacificaAdapter.setNoAgent(true);
|
|
118
150
|
_adapter = _pacificaAdapter;
|
|
119
151
|
break;
|
|
120
152
|
}
|
|
@@ -125,43 +157,79 @@ async function getAdapter() {
|
|
|
125
157
|
await _hlAdapter.init();
|
|
126
158
|
if (pk) {
|
|
127
159
|
const hlSettings = loadSettings();
|
|
128
|
-
if (
|
|
160
|
+
if (!hlSettings.referralApplied.hyperliquid) {
|
|
129
161
|
const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
|
|
130
162
|
if (hlRef) {
|
|
131
163
|
_hlAdapter.autoSetReferrer(hlRef).then(() => {
|
|
132
164
|
const s = loadSettings();
|
|
133
165
|
s.referralApplied.hyperliquid = true;
|
|
134
166
|
saveSettings(s);
|
|
135
|
-
}).catch(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
}).catch((err) => {
|
|
168
|
+
// Per SSOT Rule #2: do NOT mark applied=true on failure.
|
|
169
|
+
// Leave referralApplied[ex]=false so next adapter init retries.
|
|
170
|
+
process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
139
171
|
});
|
|
140
172
|
}
|
|
141
173
|
}
|
|
142
174
|
}
|
|
175
|
+
// Tier 2: OWS master if --ows or settings.owsActiveWallet
|
|
176
|
+
const hlOwsName = owsName || _settings.owsActiveWallet;
|
|
177
|
+
if (hlOwsName && !pk) {
|
|
178
|
+
const { OwsEvmSigner } = await import("./signer/ows-evm.js");
|
|
179
|
+
const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
|
|
180
|
+
const pp = await resolvePassphrase({ flag: opts.passphrase });
|
|
181
|
+
if (pp !== null)
|
|
182
|
+
_hlAdapter.setSigner(OwsEvmSigner.create(hlOwsName, pp));
|
|
183
|
+
}
|
|
184
|
+
// Tier 1: agent if registered
|
|
185
|
+
const { getAgent: getHlAgent } = await import("./agent-wallet/store.js");
|
|
186
|
+
const hlAgentMeta = getHlAgent("hyperliquid");
|
|
187
|
+
if (hlAgentMeta) {
|
|
188
|
+
const { OwsEvmSigner: HlOwsEvmSigner } = await import("./signer/ows-evm.js");
|
|
189
|
+
// HL agent wallet was created with empty passphrase (see runHlApproveFlow);
|
|
190
|
+
// open with "" to match. Agent at-rest encryption handled by OWS storage layer.
|
|
191
|
+
const agentSigner = HlOwsEvmSigner.create(hlAgentMeta.agentWalletName, "");
|
|
192
|
+
_hlAdapter.setAgentSigner(hlAgentMeta, agentSigner);
|
|
193
|
+
}
|
|
194
|
+
if (noAgent)
|
|
195
|
+
_hlAdapter.setNoAgent(true);
|
|
143
196
|
_adapter = _hlAdapter;
|
|
144
197
|
break;
|
|
145
198
|
}
|
|
146
199
|
case "lighter": {
|
|
147
200
|
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
201
|
+
// Phase 2d (SSOT Rule #3): when an agent is registered AND not bypassed,
|
|
202
|
+
// load the agent's L2 secp256k1 key from the encrypted keystore at
|
|
203
|
+
// ~/.perp/lighter-agents/<accountIndex>-<slot>.json BEFORE init() so the
|
|
204
|
+
// WASM client is created with the agent identity. Missing keystore →
|
|
205
|
+
// loadLighterKey throws LIGHTER_KEYSTORE_NOT_FOUND with remediation
|
|
206
|
+
// (no fallback to env per SSOT Rule #2).
|
|
207
|
+
const { getAgent: getLtAgent } = await import("./agent-wallet/store.js");
|
|
208
|
+
const ltAgentMeta = getLtAgent("lighter");
|
|
148
209
|
_lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
|
|
210
|
+
if (ltAgentMeta && !noAgent) {
|
|
211
|
+
const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
|
|
212
|
+
const ltL2Key = loadLighterKey(ltAgentMeta.accountIndex, ltAgentMeta.apiKeyIndex, "");
|
|
213
|
+
_lighterAdapter.setAgentSigner(ltAgentMeta, ltL2Key);
|
|
214
|
+
}
|
|
215
|
+
if (noAgent)
|
|
216
|
+
_lighterAdapter.setNoAgent(true);
|
|
149
217
|
await _lighterAdapter.init();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
}
|
|
218
|
+
// LT referral apply is L2-signed (POST /referral/use uses the WASM slot
|
|
219
|
+
// signer's auth token, not the master EVM). Trigger on any active
|
|
220
|
+
// signer tier — agent / OWS master / PK direct — gated by isReadOnly.
|
|
221
|
+
const ltSettings = loadSettings();
|
|
222
|
+
if (!ltSettings.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
|
|
223
|
+
const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
|
|
224
|
+
if (ltRef) {
|
|
225
|
+
_lighterAdapter.useReferralCode(ltRef).then(() => {
|
|
226
|
+
const s = loadSettings();
|
|
227
|
+
s.referralApplied.lighter = true;
|
|
228
|
+
saveSettings(s);
|
|
229
|
+
}).catch((err) => {
|
|
230
|
+
// Per SSOT Rule #2: do NOT mark applied=true on failure.
|
|
231
|
+
process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
232
|
+
});
|
|
165
233
|
}
|
|
166
234
|
}
|
|
167
235
|
_adapter = _lighterAdapter;
|
|
@@ -169,8 +237,27 @@ async function getAdapter() {
|
|
|
169
237
|
}
|
|
170
238
|
case "aster": {
|
|
171
239
|
const { AsterAdapter } = await import("./exchanges/aster.js");
|
|
172
|
-
const ast = new AsterAdapter(
|
|
240
|
+
const ast = new AsterAdapter(pk ?? undefined, isTestnet); // Tier 3 from PK
|
|
173
241
|
await ast.init();
|
|
242
|
+
// Tier 2: OWS master if --ows or settings.owsActiveWallet
|
|
243
|
+
const asterOwsName = owsName || _settings.owsActiveWallet;
|
|
244
|
+
if (asterOwsName) {
|
|
245
|
+
const { OwsEvmSigner } = await import("./signer/ows-evm.js");
|
|
246
|
+
const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
|
|
247
|
+
const pp = await resolvePassphrase({ flag: opts.passphrase });
|
|
248
|
+
if (pp !== null)
|
|
249
|
+
ast.setMasterSigner(OwsEvmSigner.create(asterOwsName, pp));
|
|
250
|
+
}
|
|
251
|
+
// Tier 1: agent if registered
|
|
252
|
+
const { getAgent: getAsterAgent } = await import("./agent-wallet/store.js");
|
|
253
|
+
const agentMeta = getAsterAgent("aster");
|
|
254
|
+
if (agentMeta) {
|
|
255
|
+
const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
|
|
256
|
+
const strat = agentSigningStrategyFor(agentMeta, owsKeyToken ?? "");
|
|
257
|
+
ast.setAgent(agentMeta, strat);
|
|
258
|
+
}
|
|
259
|
+
if (noAgent)
|
|
260
|
+
ast.setNoAgent(true);
|
|
174
261
|
_adapter = ast;
|
|
175
262
|
break;
|
|
176
263
|
}
|
|
@@ -185,6 +272,8 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
|
|
|
185
272
|
const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
|
|
186
273
|
// If an OWS API key token is provided, use it as passphrase → routes through policy engine
|
|
187
274
|
const passphrase = owsKeyToken || "";
|
|
275
|
+
// Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
|
|
276
|
+
const noAgent = opts.agent === false;
|
|
188
277
|
switch (exchange) {
|
|
189
278
|
case "pacifica": {
|
|
190
279
|
const pacNetwork = (isTestnet ? "testnet" : "mainnet");
|
|
@@ -193,6 +282,15 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
|
|
|
193
282
|
const dummyKeypair = Keypair.generate();
|
|
194
283
|
_pacificaAdapter = new PacificaAdapter(dummyKeypair, pacNetwork, builderCode, true);
|
|
195
284
|
_pacificaAdapter.setSigner(OwsSolanaSigner.create(owsWalletName, passphrase));
|
|
285
|
+
// Tier 1: agent if registered (Phase 2c)
|
|
286
|
+
const { getAgent: getPacAgentOws } = await import("./agent-wallet/store.js");
|
|
287
|
+
const pacAgentMetaOws = getPacAgentOws("pacifica");
|
|
288
|
+
if (pacAgentMetaOws) {
|
|
289
|
+
const agentSignerOws = OwsSolanaSigner.create(pacAgentMetaOws.agentWalletName, "");
|
|
290
|
+
_pacificaAdapter.setAgentSigner(pacAgentMetaOws, agentSignerOws);
|
|
291
|
+
}
|
|
292
|
+
if (noAgent)
|
|
293
|
+
_pacificaAdapter.setNoAgent(true);
|
|
196
294
|
_adapter = _pacificaAdapter;
|
|
197
295
|
return _adapter;
|
|
198
296
|
}
|
|
@@ -213,6 +311,23 @@ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToke
|
|
|
213
311
|
_adapter = _lighterAdapter;
|
|
214
312
|
return _adapter;
|
|
215
313
|
}
|
|
314
|
+
case "aster": {
|
|
315
|
+
const { AsterAdapter } = await import("./exchanges/aster.js");
|
|
316
|
+
const asterOws = new AsterAdapter(undefined, isTestnet);
|
|
317
|
+
await asterOws.init();
|
|
318
|
+
asterOws.setMasterSigner(OwsEvmSigner.create(owsWalletName, passphrase));
|
|
319
|
+
// Optionally also set agent if registered
|
|
320
|
+
const { getAgent: getAsterAgentOws } = await import("./agent-wallet/store.js");
|
|
321
|
+
const asterMeta = getAsterAgentOws("aster");
|
|
322
|
+
if (asterMeta) {
|
|
323
|
+
const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
|
|
324
|
+
asterOws.setAgent(asterMeta, agentSigningStrategyFor(asterMeta, owsKeyToken ?? ""));
|
|
325
|
+
}
|
|
326
|
+
if (noAgent)
|
|
327
|
+
asterOws.setNoAgent(true);
|
|
328
|
+
_adapter = asterOws;
|
|
329
|
+
return _adapter;
|
|
330
|
+
}
|
|
216
331
|
default:
|
|
217
332
|
throw new Error(`Unknown exchange: ${exchange}`);
|
|
218
333
|
}
|
|
@@ -244,13 +359,11 @@ function getHLAdapter() {
|
|
|
244
359
|
registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
245
360
|
registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
246
361
|
registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
|
|
247
|
-
|
|
362
|
+
// manage tree wired inside registerWalletCommands below.
|
|
248
363
|
// stream commands removed
|
|
249
364
|
registerArbCommands(program, isJson, getAdapterForExchange);
|
|
250
|
-
registerWalletCommands(program, isJson);
|
|
251
|
-
|
|
252
|
-
registerBridgeCommands(program, isJson);
|
|
253
|
-
registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
|
|
365
|
+
registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter);
|
|
366
|
+
registerFundsCommands(program, getAdapter, isJson, () => program.opts().network, getAdapterForExchange);
|
|
254
367
|
// alert commands removed
|
|
255
368
|
// Helper to get adapter for a specific exchange (used by arb-auto)
|
|
256
369
|
async function getAdapterForExchange(rawExchange) {
|
|
@@ -258,8 +371,19 @@ async function getAdapterForExchange(rawExchange) {
|
|
|
258
371
|
const opts = program.opts();
|
|
259
372
|
const network = opts.network;
|
|
260
373
|
const isTestnet = network === "testnet";
|
|
261
|
-
const
|
|
374
|
+
const owsName = opts.ows;
|
|
375
|
+
const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
|
|
376
|
+
const walletName = owsName ? `ows:${owsName}` : opts.wallet;
|
|
377
|
+
// Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
|
|
378
|
+
const noAgent = opts.agent === false;
|
|
262
379
|
const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
|
|
380
|
+
// Route through OWS init when pk is an OWS reference (e.g. "ows:main").
|
|
381
|
+
// Mirrors getAdapter() at the top of this file. Without this, adapters
|
|
382
|
+
// would try to parse "ows:main" as a raw private key and ethers would
|
|
383
|
+
// throw "invalid BytesLike value" / parseSolanaKeypair would fail.
|
|
384
|
+
if (pk && isOwsKey(pk)) {
|
|
385
|
+
return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
|
|
386
|
+
}
|
|
263
387
|
switch (exchange) {
|
|
264
388
|
case "pacifica": {
|
|
265
389
|
if (_pacificaAdapter)
|
|
@@ -269,6 +393,18 @@ async function getAdapterForExchange(rawExchange) {
|
|
|
269
393
|
const s1 = loadSettings();
|
|
270
394
|
const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
|
|
271
395
|
_pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
|
|
396
|
+
// Tier 1: agent if registered (Phase 2c)
|
|
397
|
+
const { getAgent: getPacAgentEx } = await import("./agent-wallet/store.js");
|
|
398
|
+
const pacAgentMetaEx = getPacAgentEx("pacifica");
|
|
399
|
+
if (pacAgentMetaEx) {
|
|
400
|
+
const { OwsSolanaSigner: PacOwsSolanaSignerEx } = await import("./signer/ows-solana.js");
|
|
401
|
+
// Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
|
|
402
|
+
// open with "" to match. Agent at-rest encryption handled by OWS storage layer.
|
|
403
|
+
const agentSignerEx = PacOwsSolanaSignerEx.create(pacAgentMetaEx.agentWalletName, "");
|
|
404
|
+
_pacificaAdapter.setAgentSigner(pacAgentMetaEx, agentSignerEx);
|
|
405
|
+
}
|
|
406
|
+
if (noAgent)
|
|
407
|
+
_pacificaAdapter.setNoAgent(true);
|
|
272
408
|
if (!_adapter)
|
|
273
409
|
_adapter = _pacificaAdapter;
|
|
274
410
|
return _pacificaAdapter;
|
|
@@ -282,21 +418,35 @@ async function getAdapterForExchange(rawExchange) {
|
|
|
282
418
|
await _hlAdapter.init();
|
|
283
419
|
if (pk) {
|
|
284
420
|
const s2 = loadSettings();
|
|
285
|
-
if (
|
|
421
|
+
if (!s2.referralApplied.hyperliquid) {
|
|
286
422
|
const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
|
|
287
423
|
if (hlRef) {
|
|
288
424
|
_hlAdapter.autoSetReferrer(hlRef).then(() => {
|
|
289
425
|
const s = loadSettings();
|
|
290
426
|
s.referralApplied.hyperliquid = true;
|
|
291
427
|
saveSettings(s);
|
|
292
|
-
}).catch(() => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
428
|
+
}).catch((err) => {
|
|
429
|
+
// Per SSOT Rule #2: do NOT mark applied=true on failure.
|
|
430
|
+
// Leave referralApplied[ex]=false so next adapter init retries.
|
|
431
|
+
process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
296
432
|
});
|
|
297
433
|
}
|
|
298
434
|
}
|
|
299
435
|
}
|
|
436
|
+
// Tier 1: agent if registered
|
|
437
|
+
{
|
|
438
|
+
const { getAgent: getHlAgent2 } = await import("./agent-wallet/store.js");
|
|
439
|
+
const hlAgentMeta2 = getHlAgent2("hyperliquid");
|
|
440
|
+
if (hlAgentMeta2) {
|
|
441
|
+
const { OwsEvmSigner: HlOwsEvmSigner2 } = await import("./signer/ows-evm.js");
|
|
442
|
+
// HL agent wallet was created with empty passphrase (see runHlApproveFlow);
|
|
443
|
+
// open with "" to match. Agent at-rest encryption handled by OWS storage layer.
|
|
444
|
+
const agentSigner2 = HlOwsEvmSigner2.create(hlAgentMeta2.agentWalletName, "");
|
|
445
|
+
_hlAdapter.setAgentSigner(hlAgentMeta2, agentSigner2);
|
|
446
|
+
}
|
|
447
|
+
if (noAgent)
|
|
448
|
+
_hlAdapter.setNoAgent(true);
|
|
449
|
+
}
|
|
300
450
|
if (!_adapter)
|
|
301
451
|
_adapter = _hlAdapter;
|
|
302
452
|
return _hlAdapter;
|
|
@@ -305,23 +455,32 @@ async function getAdapterForExchange(rawExchange) {
|
|
|
305
455
|
if (_lighterAdapter)
|
|
306
456
|
return _lighterAdapter;
|
|
307
457
|
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
458
|
+
const { getAgent: getLtAgentEx } = await import("./agent-wallet/store.js");
|
|
459
|
+
const ltAgentMetaEx = getLtAgentEx("lighter");
|
|
308
460
|
_lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
|
|
461
|
+
if (ltAgentMetaEx && !noAgent) {
|
|
462
|
+
// SSOT Rule #3: load agent L2 key from encrypted keystore — no env fallback.
|
|
463
|
+
const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
|
|
464
|
+
const ltL2KeyEx = loadLighterKey(ltAgentMetaEx.accountIndex, ltAgentMetaEx.apiKeyIndex, "");
|
|
465
|
+
_lighterAdapter.setAgentSigner(ltAgentMetaEx, ltL2KeyEx);
|
|
466
|
+
}
|
|
467
|
+
if (noAgent)
|
|
468
|
+
_lighterAdapter.setNoAgent(true);
|
|
309
469
|
await _lighterAdapter.init();
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
470
|
+
// LT referral apply is L2-signed; trigger on any active tier (agent
|
|
471
|
+
// included), gated by isReadOnly. See first LT case for rationale.
|
|
472
|
+
const s3 = loadSettings();
|
|
473
|
+
if (!s3.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
|
|
474
|
+
const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
|
|
475
|
+
if (ltRef) {
|
|
476
|
+
_lighterAdapter.useReferralCode(ltRef).then(() => {
|
|
477
|
+
const s = loadSettings();
|
|
478
|
+
s.referralApplied.lighter = true;
|
|
479
|
+
saveSettings(s);
|
|
480
|
+
}).catch((err) => {
|
|
481
|
+
// Per SSOT Rule #2: do NOT mark applied=true on failure.
|
|
482
|
+
process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
483
|
+
});
|
|
325
484
|
}
|
|
326
485
|
}
|
|
327
486
|
if (!_adapter)
|
|
@@ -330,9 +489,30 @@ async function getAdapterForExchange(rawExchange) {
|
|
|
330
489
|
}
|
|
331
490
|
case "aster": {
|
|
332
491
|
const { AsterAdapter } = await import("./exchanges/aster.js");
|
|
333
|
-
const
|
|
334
|
-
await
|
|
335
|
-
|
|
492
|
+
const astEx = new AsterAdapter(pk ?? undefined, isTestnet);
|
|
493
|
+
await astEx.init();
|
|
494
|
+
// Tier 2: OWS master if --ows or settings.owsActiveWallet
|
|
495
|
+
const asterExOwsName = opts.ows || _settings.owsActiveWallet;
|
|
496
|
+
if (asterExOwsName) {
|
|
497
|
+
const { OwsEvmSigner } = await import("./signer/ows-evm.js");
|
|
498
|
+
const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
|
|
499
|
+
const pp = await resolvePassphrase({ flag: opts.passphrase });
|
|
500
|
+
if (pp !== null)
|
|
501
|
+
astEx.setMasterSigner(OwsEvmSigner.create(asterExOwsName, pp));
|
|
502
|
+
}
|
|
503
|
+
// Tier 1: agent if registered
|
|
504
|
+
const owsKeyEx = opts.owsKey || process.env.OWS_API_KEY;
|
|
505
|
+
const { getAgent: getAsterAgentEx } = await import("./agent-wallet/store.js");
|
|
506
|
+
const agentMetaEx = getAsterAgentEx("aster");
|
|
507
|
+
if (agentMetaEx) {
|
|
508
|
+
const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
|
|
509
|
+
astEx.setAgent(agentMetaEx, agentSigningStrategyFor(agentMetaEx, owsKeyEx ?? ""));
|
|
510
|
+
}
|
|
511
|
+
if (noAgent)
|
|
512
|
+
astEx.setNoAgent(true);
|
|
513
|
+
if (!_adapter)
|
|
514
|
+
_adapter = astEx;
|
|
515
|
+
return astEx;
|
|
336
516
|
}
|
|
337
517
|
default:
|
|
338
518
|
throw new Error(`Unknown exchange: ${exchange}`);
|
|
@@ -355,312 +535,27 @@ async function getHLAdapterForDex(dex) {
|
|
|
355
535
|
}
|
|
356
536
|
registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
|
|
357
537
|
registerArbManageCommands(program, getAdapterForExchange, isJson);
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
538
|
+
// `agent` is no longer top-level — registered as `wallet agent` inside
|
|
539
|
+
// registerWalletCommands above.
|
|
540
|
+
// withdraw merged into funds.
|
|
541
|
+
// `rebalance` is no longer top-level — registered as `funds rebalance` inside
|
|
542
|
+
// registerFundsCommands above.
|
|
543
|
+
// Background processes & strategies
|
|
544
|
+
import { registerBackgroundCommands } from "./commands/jobs.js";
|
|
545
|
+
registerBackgroundCommands(program, isJson);
|
|
546
|
+
registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
|
|
365
547
|
// Agent-friendly commands
|
|
366
548
|
registerRiskCommands(program, getAdapterForExchange, isJson);
|
|
367
549
|
registerHistoryCommands(program, isJson, getAdapterForExchange);
|
|
368
550
|
registerSettingsCommands(program, isJson, getAdapterForExchange);
|
|
369
551
|
// dex commands removed — use 'market hip3' + --dex flag
|
|
370
|
-
|
|
552
|
+
// `plan` is no longer top-level — registered as `strategy plan` inside
|
|
553
|
+
// registerStrategyCommands above.
|
|
371
554
|
// funding merged into arb — registerFundingCommands removed
|
|
372
555
|
registerBacktestCommands(program, isJson);
|
|
373
|
-
|
|
556
|
+
registerPortfolioCommand(program, getAdapterForExchange, isJson, _pkg.version, getHLAdapterForDex);
|
|
374
557
|
registerInitCommand(program);
|
|
375
558
|
registerAlertCommands(program, isJson);
|
|
376
|
-
// Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
|
|
377
|
-
const apiSpecCmd = program
|
|
378
|
-
.command("api-spec")
|
|
379
|
-
.description("Use 'perp agent schema'")
|
|
380
|
-
.action(async () => {
|
|
381
|
-
const { jsonOk, printJson } = await import("./utils.js");
|
|
382
|
-
const { getCliSpec } = await import("./cli-spec.js");
|
|
383
|
-
printJson(jsonOk(getCliSpec(program)));
|
|
384
|
-
});
|
|
385
|
-
apiSpecCmd._hidden = true;
|
|
386
|
-
// Unified dashboard: balances + positions + top arb opportunities
|
|
387
|
-
program.command("status")
|
|
388
|
-
.description("Unified dashboard: balances, positions, and top arb opportunities")
|
|
389
|
-
.option("--health", "Check connectivity only")
|
|
390
|
-
.action(async (opts) => {
|
|
391
|
-
if (opts.health) {
|
|
392
|
-
const { runHealthCheck } = await import("./commands/risk.js");
|
|
393
|
-
return runHealthCheck(isJson);
|
|
394
|
-
}
|
|
395
|
-
const json = isJson();
|
|
396
|
-
const { formatUsd, formatPnl, makeTable, printJson, jsonOk, withJsonErrors } = await import("./utils.js");
|
|
397
|
-
const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
|
|
398
|
-
const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
|
|
399
|
-
await withJsonErrors(json, async () => {
|
|
400
|
-
const EX_LIST = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
401
|
-
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : "AST";
|
|
402
|
-
const snapshotPromises = EX_LIST.map(async (ex) => {
|
|
403
|
-
try {
|
|
404
|
-
const adapter = await getAdapterForExchange(ex);
|
|
405
|
-
const [balance, positions, orders] = await Promise.all([
|
|
406
|
-
adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(),
|
|
407
|
-
]);
|
|
408
|
-
// Fetch spot balances for HL and LT (Pacifica is perp-only)
|
|
409
|
-
let spotHoldings = [];
|
|
410
|
-
let isUnified = false;
|
|
411
|
-
try {
|
|
412
|
-
if (ex === "hyperliquid") {
|
|
413
|
-
const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
|
|
414
|
-
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
415
|
-
await hlSpot.init();
|
|
416
|
-
const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
|
|
417
|
-
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
418
|
-
const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
|
|
419
|
-
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => {
|
|
420
|
-
const base = strip(b.token);
|
|
421
|
-
return { ...b, valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total) };
|
|
422
|
-
});
|
|
423
|
-
isUnified = !adapter.dex;
|
|
424
|
-
}
|
|
425
|
-
else if (ex === "lighter") {
|
|
426
|
-
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
427
|
-
const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
|
|
428
|
-
const ltSpot = new LighterSpotAdapter(adapter);
|
|
429
|
-
await ltSpot.init();
|
|
430
|
-
const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
|
|
431
|
-
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
432
|
-
spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => ({
|
|
433
|
-
...b,
|
|
434
|
-
valueUsd: b.token === "USDC" || b.token === "USDC_SPOT" ? Number(b.total) : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
|
|
435
|
-
}));
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
catch { /* spot not available */ }
|
|
439
|
-
return { exchange: ex, connected: true, balance, positions, openOrders: orders.length, spotHoldings, isUnified, error: undefined };
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
return { exchange: ex, connected: false, balance: null, positions: [], openOrders: 0, spotHoldings: [], isUnified: false, error: err instanceof Error ? err.message : String(err) };
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
const arbPromise = fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 }).catch(() => null);
|
|
446
|
-
const [snapshots, arbSnapshot] = await Promise.all([Promise.all(snapshotPromises), arbPromise]);
|
|
447
|
-
// Save funding snapshot for sparkline history
|
|
448
|
-
if (arbSnapshot) {
|
|
449
|
-
try {
|
|
450
|
-
const allRates = arbSnapshot.symbols.flatMap(s => s.rates);
|
|
451
|
-
if (allRates.length > 0)
|
|
452
|
-
saveFundingSnapshot(allRates);
|
|
453
|
-
}
|
|
454
|
-
catch { /* */ }
|
|
455
|
-
}
|
|
456
|
-
// Build totals (include non-USDC spot value for unified accounts)
|
|
457
|
-
let totalEquity = 0;
|
|
458
|
-
let totalSpotValue = 0;
|
|
459
|
-
const allPositions = [];
|
|
460
|
-
const allSpotHoldings = [];
|
|
461
|
-
for (const s of snapshots) {
|
|
462
|
-
if (s.balance)
|
|
463
|
-
totalEquity += Number(s.balance.equity);
|
|
464
|
-
for (const p of s.positions)
|
|
465
|
-
allPositions.push({ ...p, exchange: s.exchange });
|
|
466
|
-
// For unified accounts (HL), USDC is already in perp equity — only add non-USDC spot
|
|
467
|
-
const displaySpot = s.isUnified
|
|
468
|
-
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
469
|
-
: s.spotHoldings;
|
|
470
|
-
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
471
|
-
totalSpotValue += spotVal;
|
|
472
|
-
totalEquity += spotVal;
|
|
473
|
-
for (const b of displaySpot)
|
|
474
|
-
allSpotHoldings.push({ ...b, exchange: s.exchange });
|
|
475
|
-
}
|
|
476
|
-
// Top arb
|
|
477
|
-
const topArb = arbSnapshot
|
|
478
|
-
? arbSnapshot.symbols.filter(s => s.maxSpreadAnnual >= 5).sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual).slice(0, 5)
|
|
479
|
-
: [];
|
|
480
|
-
// Risk
|
|
481
|
-
const marginUsed = snapshots.reduce((s, x) => s + (x.balance ? Number(x.balance.marginUsed) : 0), 0);
|
|
482
|
-
const marginPct = totalEquity > 0 ? (marginUsed / totalEquity) * 100 : 0;
|
|
483
|
-
const riskLevel = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
|
|
484
|
-
if (json) {
|
|
485
|
-
const now = new Date();
|
|
486
|
-
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
487
|
-
return printJson(jsonOk({
|
|
488
|
-
version: _pkg.version,
|
|
489
|
-
totalEquity,
|
|
490
|
-
riskLevel,
|
|
491
|
-
exchanges: snapshots.map(s => ({
|
|
492
|
-
name: s.exchange, connected: s.connected,
|
|
493
|
-
equity: s.balance ? Number(s.balance.equity) : 0,
|
|
494
|
-
available: s.balance ? Number(s.balance.available) : 0,
|
|
495
|
-
spotHoldings: s.spotHoldings.filter(b => Number(b.total) > 0).map(b => ({
|
|
496
|
-
token: b.token, total: b.total, available: b.available, valueUsd: b.valueUsd,
|
|
497
|
-
})),
|
|
498
|
-
})),
|
|
499
|
-
totalSpotValue,
|
|
500
|
-
positions: allPositions,
|
|
501
|
-
topArbOpportunities: await (async () => {
|
|
502
|
-
const { getHistoricalAverages: getAvgs } = await import("./funding-history.js");
|
|
503
|
-
const syms = topArb.map(a => a.symbol);
|
|
504
|
-
const avgs = syms.length > 0 ? getAvgs(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
|
|
505
|
-
return topArb.map(a => {
|
|
506
|
-
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
507
|
-
const avg = avgs.get(`${a.symbol}:${bestEx}`);
|
|
508
|
-
return {
|
|
509
|
-
symbol: a.symbol, spreadAnnual: a.maxSpreadAnnual,
|
|
510
|
-
direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
|
|
511
|
-
avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
|
|
512
|
-
avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
|
|
513
|
-
rateHistory: getHistoricalRates(a.symbol, bestEx, h24, now).map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })),
|
|
514
|
-
};
|
|
515
|
-
});
|
|
516
|
-
})(),
|
|
517
|
-
}));
|
|
518
|
-
}
|
|
519
|
-
// ── Terminal UI ──
|
|
520
|
-
console.log(chalk.cyan.bold(`\n perp-cli v${_pkg.version}`) + chalk.gray(` — ${snapshots.filter(s => s.connected).length} exchanges connected\n`));
|
|
521
|
-
// Balance bars + Top Arb side by side
|
|
522
|
-
const now = new Date();
|
|
523
|
-
const h24 = new Date(now.getTime() - 24 * 3600_000);
|
|
524
|
-
// Left column: Balances (perp + spot per exchange)
|
|
525
|
-
// Compute per-exchange totals: perp equity + non-USDC spot (unified) or all spot
|
|
526
|
-
const exTotals = snapshots.map(s => {
|
|
527
|
-
if (!s.connected || !s.balance)
|
|
528
|
-
return { total: 0, margin: 0, equity: 0 };
|
|
529
|
-
const equity = Number(s.balance.equity);
|
|
530
|
-
const margin = Number(s.balance.marginUsed);
|
|
531
|
-
const displaySpot = s.isUnified
|
|
532
|
-
? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
533
|
-
: s.spotHoldings;
|
|
534
|
-
const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
|
|
535
|
-
return { total: equity + spotVal, margin, equity };
|
|
536
|
-
});
|
|
537
|
-
const balLines = [];
|
|
538
|
-
balLines.push(chalk.white.bold(" Balances"));
|
|
539
|
-
for (let i = 0; i < snapshots.length; i++) {
|
|
540
|
-
const s = snapshots[i];
|
|
541
|
-
if (!s.connected) {
|
|
542
|
-
balLines.push(` ${chalk.gray(exAbbr(s.exchange).padEnd(4))} ${chalk.red("disconnected")}`);
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
const { total, margin, equity } = exTotals[i];
|
|
546
|
-
const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
|
|
547
|
-
const barFull = Math.min(20, Math.round(usagePct / 5));
|
|
548
|
-
const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
|
|
549
|
-
const bar = usageColor("\u2588".repeat(barFull)) + chalk.gray("\u2591".repeat(20 - barFull));
|
|
550
|
-
balLines.push(` ${chalk.white.bold(exAbbr(s.exchange).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
|
|
551
|
-
}
|
|
552
|
-
// Spot holdings (non-USDC tokens with value)
|
|
553
|
-
const displaySpotHoldings = allSpotHoldings.filter(b => {
|
|
554
|
-
const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
|
|
555
|
-
return tk !== "USDC";
|
|
556
|
-
});
|
|
557
|
-
if (displaySpotHoldings.length > 0) {
|
|
558
|
-
balLines.push("");
|
|
559
|
-
balLines.push(chalk.white.bold(" Spot Holdings"));
|
|
560
|
-
for (const b of displaySpotHoldings) {
|
|
561
|
-
const token = b.token.replace(/-SPOT$/i, "");
|
|
562
|
-
const ex = exAbbr(b.exchange);
|
|
563
|
-
balLines.push(` ${chalk.gray(ex.padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const riskColor = riskLevel === "LOW" ? chalk.green : riskLevel === "MEDIUM" ? chalk.yellow : chalk.red;
|
|
567
|
-
balLines.push(` ${"─".repeat(44)}`);
|
|
568
|
-
balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(totalEquity)}`.padEnd(9))} Risk: ${riskColor(riskLevel)}`);
|
|
569
|
-
// Right column: Top Arb Opportunities (with 24h/7d averages from local history — 0 API calls)
|
|
570
|
-
const { getHistoricalAverages } = await import("./funding-history.js");
|
|
571
|
-
const arbSymbols = topArb.map(a => a.symbol);
|
|
572
|
-
const arbExchanges = ["hyperliquid", "pacifica", "lighter"];
|
|
573
|
-
const histAvgs = arbSymbols.length > 0 ? getHistoricalAverages(arbSymbols, arbExchanges) : new Map();
|
|
574
|
-
const arbLines = [];
|
|
575
|
-
arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
|
|
576
|
-
if (topArb.length > 0) {
|
|
577
|
-
// SYMBOL SPREAD 24h TREND 7d DIR
|
|
578
|
-
arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "".padEnd(9) + "7d".padEnd(7)));
|
|
579
|
-
for (const a of topArb) {
|
|
580
|
-
const spreadColor = a.maxSpreadAnnual >= 100 ? chalk.green.bold : a.maxSpreadAnnual >= 30 ? chalk.green : chalk.yellow;
|
|
581
|
-
const dir = `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`;
|
|
582
|
-
const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
|
|
583
|
-
// 24h trend arrow
|
|
584
|
-
const history = getHistoricalRates(a.symbol, bestEx, h24, now);
|
|
585
|
-
let trend = "";
|
|
586
|
-
if (history.length >= 2) {
|
|
587
|
-
const oldAnn = Math.abs(history[0].hourlyRate) * 8760 * 100;
|
|
588
|
-
const newAnn = Math.abs(history[history.length - 1].hourlyRate) * 8760 * 100;
|
|
589
|
-
const delta = newAnn - oldAnn;
|
|
590
|
-
const abs = Math.abs(delta);
|
|
591
|
-
trend = abs < 1 ? chalk.gray(`\u2500${abs.toFixed(0)}%`)
|
|
592
|
-
: delta > 0 ? chalk.red(`\u25B2+${abs.toFixed(0)}%`)
|
|
593
|
-
: chalk.green(`\u25BC-${abs.toFixed(0)}%`);
|
|
594
|
-
}
|
|
595
|
-
// 24h and 7d averages (annualized from hourly)
|
|
596
|
-
const avg = histAvgs.get(`${a.symbol}:${bestEx}`);
|
|
597
|
-
const avg24h = avg?.avg24h != null ? `${(Math.abs(avg.avg24h) * 8760 * 100).toFixed(0)}%` : "-";
|
|
598
|
-
const avg7d = avg?.avg7d != null ? `${(Math.abs(avg.avg7d) * 8760 * 100).toFixed(0)}%` : "-";
|
|
599
|
-
arbLines.push(` ${chalk.white.bold(a.symbol.padEnd(8))} ${spreadColor(`${a.maxSpreadAnnual.toFixed(1)}%`.padEnd(9))}${chalk.gray(avg24h.padEnd(7))}${trend.padEnd(9)}${chalk.gray(avg7d.padEnd(7))}${chalk.gray(dir)}`);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
arbLines.push(chalk.gray(" No opportunities above 5%"));
|
|
604
|
-
}
|
|
605
|
-
// Print layout — side-by-side if terminal is wide enough, else stacked
|
|
606
|
-
const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
607
|
-
const termW = process.stdout.columns || 80;
|
|
608
|
-
const SIDE_BY_SIDE_MIN = 100; // 2 indent + 1 + 46 + 1 + 48 + 1
|
|
609
|
-
if (termW >= SIDE_BY_SIDE_MIN) {
|
|
610
|
-
const LEFT_W = 46;
|
|
611
|
-
const RIGHT_W = 48;
|
|
612
|
-
const maxLines = Math.max(balLines.length, arbLines.length);
|
|
613
|
-
console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
|
|
614
|
-
for (let i = 0; i < maxLines; i++) {
|
|
615
|
-
const left = balLines[i] ?? "";
|
|
616
|
-
const right = arbLines[i] ?? "";
|
|
617
|
-
const leftPad = LEFT_W - stripAnsi(left).length;
|
|
618
|
-
const rightPad = RIGHT_W - stripAnsi(right).length;
|
|
619
|
-
console.log(` \u2502${left}${" ".repeat(Math.max(0, leftPad))}\u2502${right}${" ".repeat(Math.max(0, rightPad))}\u2502`);
|
|
620
|
-
}
|
|
621
|
-
console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
// Stacked layout for narrow terminals
|
|
625
|
-
const boxW = Math.min(termW - 4, 74); // 2 indent + 2 borders
|
|
626
|
-
const printBox = (lines) => {
|
|
627
|
-
console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
|
|
628
|
-
for (const line of lines) {
|
|
629
|
-
const pad = boxW - stripAnsi(line).length;
|
|
630
|
-
console.log(` \u2502${line}${" ".repeat(Math.max(0, pad))}\u2502`);
|
|
631
|
-
}
|
|
632
|
-
console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
|
|
633
|
-
};
|
|
634
|
-
printBox(balLines);
|
|
635
|
-
console.log();
|
|
636
|
-
printBox(arbLines);
|
|
637
|
-
}
|
|
638
|
-
// Positions
|
|
639
|
-
if (allPositions.length > 0) {
|
|
640
|
-
console.log(chalk.white.bold("\n Positions"));
|
|
641
|
-
const posRows = allPositions.map(p => {
|
|
642
|
-
const sideColor = p.side === "long" ? chalk.green : chalk.red;
|
|
643
|
-
const notional = Math.abs(Number(p.size) * Number(p.markPrice));
|
|
644
|
-
const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
|
|
645
|
-
const warn = p.leverage >= 5 ? chalk.red(" \u26A0") : "";
|
|
646
|
-
return [
|
|
647
|
-
chalk.white.bold(p.symbol.replace("-PERP", "")),
|
|
648
|
-
chalk.gray(exAbbr(p.exchange)),
|
|
649
|
-
sideColor(p.side.toUpperCase()),
|
|
650
|
-
p.size,
|
|
651
|
-
`$${formatUsd(p.entryPrice)}\u2192$${formatUsd(p.markPrice)}`,
|
|
652
|
-
formatPnl(p.unrealizedPnl),
|
|
653
|
-
`$${formatUsd(notional)}`,
|
|
654
|
-
levStr + warn,
|
|
655
|
-
];
|
|
656
|
-
});
|
|
657
|
-
console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry\u2192Mark", "uPnL", "Notional", "Lev"], posRows));
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
console.log(chalk.gray("\n No open positions.\n"));
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
559
|
// Switch shared API URLs if --network testnet is used
|
|
665
560
|
program.hook("preAction", () => {
|
|
666
561
|
const network = program.opts().network;
|
|
@@ -777,8 +672,8 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
777
672
|
console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
|
|
778
673
|
console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
|
|
779
674
|
console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
|
|
780
|
-
console.log(` ${chalk.green("perp
|
|
781
|
-
console.log(` ${chalk.green("perp
|
|
675
|
+
console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
|
|
676
|
+
console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
|
|
782
677
|
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
783
678
|
}
|
|
784
679
|
}
|