perp-cli 0.12.5 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/account.js +21 -1
- package/dist/commands/agent.d.ts +34 -0
- package/dist/commands/agent.js +90 -0
- package/dist/commands/init.js +9 -23
- package/dist/commands/manage.js +156 -1
- package/dist/commands/wallet.js +2 -17
- package/dist/exchanges/hyperliquid.d.ts +25 -0
- package/dist/exchanges/hyperliquid.js +58 -18
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +2 -2
package/dist/commands/account.js
CHANGED
|
@@ -9,7 +9,27 @@ function pac(adapter) {
|
|
|
9
9
|
}
|
|
10
10
|
export function registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange) {
|
|
11
11
|
const account = program.command("account").description("Account commands");
|
|
12
|
-
// ──
|
|
12
|
+
// ── balance alias → redirect to portfolio (renamed in v0.12) ──
|
|
13
|
+
// Users who type `perp account balance` get a structured pointer instead of
|
|
14
|
+
// "unknown command". Per Rule #2 we don't silently retarget — we tell the
|
|
15
|
+
// user what to run and exit non-zero so scripts can detect the rename.
|
|
16
|
+
account
|
|
17
|
+
.command("balance")
|
|
18
|
+
.description("[renamed] Use 'perp portfolio' (exchange-aggregated) or 'perp portfolio -e <ex>'")
|
|
19
|
+
.action(async () => {
|
|
20
|
+
const msg = "'account balance' was renamed in v0.12 — use 'portfolio' instead.";
|
|
21
|
+
const remediation = "perp portfolio # all exchanges\nperp portfolio -e <exchange> # single exchange detail";
|
|
22
|
+
if (isJson()) {
|
|
23
|
+
const { jsonError } = await import("../utils.js");
|
|
24
|
+
const { printJson } = await import("../utils.js");
|
|
25
|
+
printJson(jsonError("INVALID_PARAMS", msg, { details: { remediation } }));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const chalk = (await import("chalk")).default;
|
|
29
|
+
console.error(chalk.yellow(`\n ${msg}`));
|
|
30
|
+
console.error(chalk.gray(` Run: ${remediation}\n`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
13
33
|
// ── HIP-3 dex helper: fetch data from all deployed dexes in parallel ──
|
|
14
34
|
async function fetchHip3Data(hlAdapter, fetcher) {
|
|
15
35
|
const dexes = await hlAdapter.listDeployedDexes();
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -7,3 +7,37 @@ import { Command } from "commander";
|
|
|
7
7
|
* relocates it to `wallet agent ...`.
|
|
8
8
|
*/
|
|
9
9
|
export declare function registerWalletAgentCommands(walletCmd: Command, isJson: () => boolean): void;
|
|
10
|
+
/** Public abstraction modes (CLI surface) → HL on-wire string. */
|
|
11
|
+
declare const HL_ABSTRACTION_TO_WIRE: {
|
|
12
|
+
readonly unified: "unifiedAccount";
|
|
13
|
+
readonly standard: "disabled";
|
|
14
|
+
readonly portfolio: "portfolioMargin";
|
|
15
|
+
};
|
|
16
|
+
export type HlAbstractionMode = keyof typeof HL_ABSTRACTION_TO_WIRE;
|
|
17
|
+
interface HlSetAbstractionFlowOpts {
|
|
18
|
+
masterName: string;
|
|
19
|
+
passphrase: string;
|
|
20
|
+
mode: HlAbstractionMode;
|
|
21
|
+
isTestnet: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface HlSetAbstractionFlowResult {
|
|
24
|
+
mode: HlAbstractionMode;
|
|
25
|
+
abstraction: string;
|
|
26
|
+
hyperliquidChain: "Mainnet" | "Testnet";
|
|
27
|
+
nonce: number;
|
|
28
|
+
userEvmAddress: `0x${string}`;
|
|
29
|
+
response: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Send a Hyperliquid `userSetAbstraction` action.
|
|
33
|
+
*
|
|
34
|
+
* Reuses the same EIP-712 user-signed domain (`HyperliquidSignTransaction`,
|
|
35
|
+
* chainId 42161) and `OwsEvmSigner.signTypedData` plumbing as
|
|
36
|
+
* `runHlApproveFlow`; the only differences are the type struct
|
|
37
|
+
* (`UserSetAbstraction` vs `ApproveAgent`) and the action payload.
|
|
38
|
+
*
|
|
39
|
+
* Throws `PerpError` with structured remediation on signature/network/venue
|
|
40
|
+
* failure (Rule #2: no fallback, no silent retry).
|
|
41
|
+
*/
|
|
42
|
+
export declare function runHlSetAbstractionFlow(opts: HlSetAbstractionFlowOpts): Promise<HlSetAbstractionFlowResult>;
|
|
43
|
+
export {};
|
package/dist/commands/agent.js
CHANGED
|
@@ -1703,6 +1703,96 @@ async function revokeHlAgent(masterSigner) {
|
|
|
1703
1703
|
});
|
|
1704
1704
|
// Revoke is best-effort — don't throw on HTTP errors
|
|
1705
1705
|
}
|
|
1706
|
+
// ── HL userSetAbstraction (account-mode write) helper ────────────────────────
|
|
1707
|
+
/** EIP-712 types for HyperliquidTransaction:UserSetAbstraction */
|
|
1708
|
+
const HL_USER_SET_ABSTRACTION_TYPES = {
|
|
1709
|
+
"HyperliquidTransaction:UserSetAbstraction": [
|
|
1710
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
1711
|
+
{ name: "abstraction", type: "string" },
|
|
1712
|
+
{ name: "nonce", type: "uint64" },
|
|
1713
|
+
],
|
|
1714
|
+
};
|
|
1715
|
+
/** Public abstraction modes (CLI surface) → HL on-wire string. */
|
|
1716
|
+
const HL_ABSTRACTION_TO_WIRE = {
|
|
1717
|
+
unified: "unifiedAccount",
|
|
1718
|
+
standard: "disabled",
|
|
1719
|
+
portfolio: "portfolioMargin",
|
|
1720
|
+
};
|
|
1721
|
+
/**
|
|
1722
|
+
* Send a Hyperliquid `userSetAbstraction` action.
|
|
1723
|
+
*
|
|
1724
|
+
* Reuses the same EIP-712 user-signed domain (`HyperliquidSignTransaction`,
|
|
1725
|
+
* chainId 42161) and `OwsEvmSigner.signTypedData` plumbing as
|
|
1726
|
+
* `runHlApproveFlow`; the only differences are the type struct
|
|
1727
|
+
* (`UserSetAbstraction` vs `ApproveAgent`) and the action payload.
|
|
1728
|
+
*
|
|
1729
|
+
* Throws `PerpError` with structured remediation on signature/network/venue
|
|
1730
|
+
* failure (Rule #2: no fallback, no silent retry).
|
|
1731
|
+
*/
|
|
1732
|
+
export async function runHlSetAbstractionFlow(opts) {
|
|
1733
|
+
const { masterName, passphrase, mode, isTestnet } = opts;
|
|
1734
|
+
const { ethers } = await import("ethers");
|
|
1735
|
+
const masterSigner = OwsEvmSigner.create(masterName, passphrase);
|
|
1736
|
+
const userEvmAddress = masterSigner.getAddress();
|
|
1737
|
+
const hyperliquidChain = (isTestnet ? "Testnet" : "Mainnet");
|
|
1738
|
+
const sigChainIdHex = "0xa4b1"; // 42161 hex
|
|
1739
|
+
const abstraction = HL_ABSTRACTION_TO_WIRE[mode];
|
|
1740
|
+
const nonce = Date.now();
|
|
1741
|
+
const action = {
|
|
1742
|
+
type: "userSetAbstraction",
|
|
1743
|
+
hyperliquidChain,
|
|
1744
|
+
signatureChainId: sigChainIdHex,
|
|
1745
|
+
user: userEvmAddress,
|
|
1746
|
+
abstraction,
|
|
1747
|
+
nonce,
|
|
1748
|
+
};
|
|
1749
|
+
const message = {
|
|
1750
|
+
hyperliquidChain,
|
|
1751
|
+
abstraction,
|
|
1752
|
+
nonce,
|
|
1753
|
+
};
|
|
1754
|
+
let sigRaw;
|
|
1755
|
+
try {
|
|
1756
|
+
sigRaw = await masterSigner.signTypedData(HL_USER_SIGNED_DOMAIN, HL_USER_SET_ABSTRACTION_TYPES, message);
|
|
1757
|
+
}
|
|
1758
|
+
catch (sigErr) {
|
|
1759
|
+
throw new PerpError("SIGNATURE_FAILED", `HL userSetAbstraction signing failed: ${sigErr instanceof Error ? sigErr.message : String(sigErr)}`, {
|
|
1760
|
+
remediation: "Confirm OWS master is unlocked and passphrase is correct.",
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
const parsed = ethers.Signature.from(sigRaw);
|
|
1764
|
+
const signature = { r: parsed.r, s: parsed.s, v: parsed.v };
|
|
1765
|
+
const baseUrl = isTestnet ? "https://api.hyperliquid-testnet.xyz" : "https://api.hyperliquid.xyz";
|
|
1766
|
+
const payload = { action, nonce, signature, vaultAddress: null };
|
|
1767
|
+
const res = await fetch(`${baseUrl}/exchange`, {
|
|
1768
|
+
method: "POST",
|
|
1769
|
+
headers: { "Content-Type": "application/json" },
|
|
1770
|
+
body: JSON.stringify(payload),
|
|
1771
|
+
});
|
|
1772
|
+
const text = await res.text();
|
|
1773
|
+
let resp;
|
|
1774
|
+
try {
|
|
1775
|
+
resp = JSON.parse(text);
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
throw new PerpError("EXCHANGE_ERROR", `HL exchange API parse error (${res.status}): ${text.slice(0, 200)}`, {
|
|
1779
|
+
remediation: "Check Hyperliquid API status. Retry: perp wallet manage account-mode <mode>",
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
if (resp?.status === "err") {
|
|
1783
|
+
throw new PerpError("EXCHANGE_ERROR", `HL userSetAbstraction failed: ${typeof resp.response === "string" ? resp.response : JSON.stringify(resp)}`, {
|
|
1784
|
+
remediation: "Verify master EVM wallet is the account owner. Retry: perp wallet manage account-mode <mode>",
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
return {
|
|
1788
|
+
mode,
|
|
1789
|
+
abstraction,
|
|
1790
|
+
hyperliquidChain,
|
|
1791
|
+
nonce,
|
|
1792
|
+
userEvmAddress,
|
|
1793
|
+
response: resp,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1706
1796
|
/**
|
|
1707
1797
|
* Core Pacifica approve flow.
|
|
1708
1798
|
* Caller MUST hold the "pacifica" lock before calling this function.
|
package/dist/commands/init.js
CHANGED
|
@@ -70,15 +70,17 @@ function askChoice(rl, question, choices) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
// ── Exchange → env var mapping ────────────────────────────────
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// (
|
|
76
|
-
//
|
|
77
|
-
//
|
|
73
|
+
// v0.12.4 retired the legacy HMAC ASTER_API_KEY/SECRET path (Plan v3.0).
|
|
74
|
+
// v0.12.6 restores Aster as a normal EVM exchange: `ASTER_PRIVATE_KEY`
|
|
75
|
+
// (Tier 3 PK direct path) is collected like HL/LT, and the agent flow
|
|
76
|
+
// (`wallet agent approve aster`) is the recommended Tier 1 onboarding.
|
|
77
|
+
// Removing Aster entirely was over-correction — the entry must exist so
|
|
78
|
+
// `wallet import` / `wallet set` / setup wizard treat all 4 DEX uniformly.
|
|
78
79
|
export const EXCHANGE_ENV_MAP = {
|
|
79
80
|
pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
|
|
80
81
|
hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
|
|
81
82
|
lighter: { envKey: "LIGHTER_PRIVATE_KEY", chain: "evm", label: "Lighter (EVM)" },
|
|
83
|
+
aster: { envKey: "ASTER_PRIVATE_KEY", chain: "evm", label: "Aster (BNB Chain)" },
|
|
82
84
|
};
|
|
83
85
|
// ── Validate keys ─────────────────────────────────────────────
|
|
84
86
|
export async function validateKey(chain, key) {
|
|
@@ -179,25 +181,9 @@ export function registerInitCommand(program) {
|
|
|
179
181
|
console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (use 'wallet agent approve aster')")}`);
|
|
180
182
|
console.log();
|
|
181
183
|
const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3,4 or 'all'): ");
|
|
182
|
-
const
|
|
183
|
-
// v0.12.4: legacy Aster HMAC API-key path is removed. The wizard can
|
|
184
|
-
// no longer collect ASTER_API_KEY/ASTER_API_SECRET — Aster onboarding
|
|
185
|
-
// is now exclusively via `perp wallet agent approve aster --master`.
|
|
186
|
-
// Print a redirect and drop Aster from the env-write set.
|
|
187
|
-
const asterRequested = selectedRaw.includes("aster");
|
|
188
|
-
const selected = selectedRaw.filter((e) => e !== "aster");
|
|
189
|
-
if (asterRequested) {
|
|
190
|
-
console.log(chalk.yellow.bold("\n Aster onboarding is via agent wallet (no API keys):"));
|
|
191
|
-
console.log(` ${chalk.green("perp wallet agent approve aster --master <name>")}`);
|
|
192
|
-
console.log(chalk.gray(" Run that after this wizard completes.\n"));
|
|
193
|
-
}
|
|
184
|
+
const selected = parseExchangeChoice(exchangeChoice);
|
|
194
185
|
if (selected.length === 0) {
|
|
195
|
-
|
|
196
|
-
console.log(chalk.gray(" No additional exchanges to configure here. See the Aster instructions above.\n"));
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
console.log(chalk.red("\n No exchanges selected.\n"));
|
|
200
|
-
}
|
|
186
|
+
console.log(chalk.red("\n No exchanges selected.\n"));
|
|
201
187
|
rl.close();
|
|
202
188
|
return;
|
|
203
189
|
}
|
package/dist/commands/manage.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { printJson, jsonOk, jsonError } from "../utils.js";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { hasPacificaSdk } from "../exchanges/capabilities.js";
|
|
4
|
+
import { PerpError } from "../errors.js";
|
|
5
|
+
import { loadSettings } from "../settings.js";
|
|
6
|
+
import { resolvePassphrase } from "../agent-wallet/passphrase.js";
|
|
7
|
+
import { runHlSetAbstractionFlow } from "./agent.js";
|
|
8
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
4
9
|
export function registerWalletManageCommands(parent, getAdapter, isJson, getPacificaAdapter) {
|
|
5
|
-
const manage = parent.command("manage").description("Exchange account settings (margin, subaccount, API keys, builder) — Pacifica + Lighter");
|
|
10
|
+
const manage = parent.command("manage").description("Exchange account settings (margin, subaccount, API keys, builder, account-mode) — Pacifica + Lighter + Hyperliquid");
|
|
6
11
|
// Ensure adapter is initialized before accessing PacificaAdapter
|
|
7
12
|
async function pac() {
|
|
8
13
|
const adapter = await getAdapter();
|
|
@@ -236,4 +241,154 @@ export function registerWalletManageCommands(parent, getAdapter, isJson, getPaci
|
|
|
236
241
|
// 3-tier signer routing, expiry tracking, free-slot picking, and AgentMeta
|
|
237
242
|
// persistence. The auto-setup path in LighterAdapter.init() remains as the
|
|
238
243
|
// env-key fallback for users who haven't run the unified command yet.
|
|
244
|
+
// ── Hyperliquid account abstraction mode ────────────────────────────────
|
|
245
|
+
// Read side: HyperliquidAdapter._getAbstractionMode() (added v0.12.7).
|
|
246
|
+
// Write side: HL `userSetAbstraction` action — master EVM EIP-712 signature.
|
|
247
|
+
// Reuses the same `OwsEvmSigner.signTypedData` path as `wallet agent approve
|
|
248
|
+
// hyperliquid` (see runHlApproveFlow). No agent signer involved: this is a
|
|
249
|
+
// master-only action that toggles the user's account-level mode.
|
|
250
|
+
manage
|
|
251
|
+
.command("account-mode [mode]")
|
|
252
|
+
.description("Hyperliquid account abstraction mode. Modes: unified | standard | portfolio. No arg = show current.")
|
|
253
|
+
.option("--master <name>", "Master OWS wallet name (defaults to settings.owsActiveWallet)")
|
|
254
|
+
.option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
|
|
255
|
+
.option("--json", "Machine-readable output")
|
|
256
|
+
.action(async (mode, opts, command) => {
|
|
257
|
+
const useJson = opts.json ?? isJson();
|
|
258
|
+
// C5 fix: parent --passphrase shadows subcommand flag in Commander v13.
|
|
259
|
+
// Merge parent + subcommand opts so --passphrase works on either side.
|
|
260
|
+
const mergedOpts = command.optsWithGlobals();
|
|
261
|
+
const passphraseFlag = mergedOpts.passphrase ?? opts.passphrase;
|
|
262
|
+
const network = mergedOpts.network ?? "mainnet";
|
|
263
|
+
const isTestnet = network === "testnet";
|
|
264
|
+
// ── Show branch (no positional arg) ─────────────────────────────────
|
|
265
|
+
if (!mode) {
|
|
266
|
+
try {
|
|
267
|
+
const adapter = new HyperliquidAdapter(undefined, isTestnet);
|
|
268
|
+
// Resolve the address. _getAbstractionMode requires an address; if
|
|
269
|
+
// the user hasn't configured one, surface a clear error instead of
|
|
270
|
+
// an opaque "address not set" exception.
|
|
271
|
+
const masterName = opts.master ?? loadSettings().owsActiveWallet;
|
|
272
|
+
if (!masterName) {
|
|
273
|
+
throw new PerpError("INVALID_PARAMS", "Master wallet name required to read account-mode. Use --master <name> or set owsActiveWallet.", {
|
|
274
|
+
remediation: "Run: perp wallet generate <name> or perp wallet use <name>, then retry.",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Resolve the EVM address from the OWS wallet without unlocking it
|
|
278
|
+
// (read-only path needs no signature).
|
|
279
|
+
const { loadOws } = await import("../signer/ows-loader.js");
|
|
280
|
+
const ows = loadOws();
|
|
281
|
+
const wallet = ows.getWallet(masterName);
|
|
282
|
+
const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155:"));
|
|
283
|
+
if (!evmAccount) {
|
|
284
|
+
throw new PerpError("INVALID_PARAMS", `OWS wallet "${masterName}" has no EVM account.`, {
|
|
285
|
+
remediation: "Generate an EVM-capable wallet: perp wallet generate <name>",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
adapter.setAddress(evmAccount.address);
|
|
289
|
+
const current = await adapter._getAbstractionMode();
|
|
290
|
+
if (useJson) {
|
|
291
|
+
return printJson(jsonOk({ mode: current, master: masterName, address: evmAccount.address, network: isTestnet ? "testnet" : "mainnet" }));
|
|
292
|
+
}
|
|
293
|
+
console.log(chalk.cyan.bold("\n Hyperliquid Account Mode\n"));
|
|
294
|
+
console.log(` Master: ${chalk.white(masterName)}`);
|
|
295
|
+
console.log(` Address: ${chalk.gray(evmAccount.address)}`);
|
|
296
|
+
console.log(` Network: ${chalk.gray(isTestnet ? "testnet" : "mainnet")}`);
|
|
297
|
+
console.log(` Mode: ${chalk.green(current)}\n`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
if (useJson) {
|
|
302
|
+
const e = err instanceof PerpError
|
|
303
|
+
? err.structured
|
|
304
|
+
: { code: "UNKNOWN", message: err instanceof Error ? err.message : String(err) };
|
|
305
|
+
console.error(JSON.stringify(jsonError(e.code, e.message)));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ── Set branch ─────────────────────────────────────────────────────
|
|
313
|
+
const modeLower = mode.toLowerCase();
|
|
314
|
+
if (modeLower !== "unified" && modeLower !== "standard" && modeLower !== "portfolio") {
|
|
315
|
+
const e = new PerpError("INVALID_PARAMS", `Mode must be one of: unified, standard, portfolio (got "${mode}")`, {
|
|
316
|
+
remediation: "Example: perp wallet manage account-mode standard --master main",
|
|
317
|
+
});
|
|
318
|
+
if (useJson) {
|
|
319
|
+
console.error(JSON.stringify(jsonError(e.structured.code, e.structured.message)));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
console.error(chalk.red(e.message));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const newMode = modeLower;
|
|
326
|
+
try {
|
|
327
|
+
const masterName = opts.master ?? loadSettings().owsActiveWallet;
|
|
328
|
+
if (!masterName) {
|
|
329
|
+
throw new PerpError("INVALID_PARAMS", "Master wallet name is required. Use --master or set owsActiveWallet in settings.", {
|
|
330
|
+
remediation: "Run: perp wallet generate <name> or perp wallet use <name>, then retry.",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
334
|
+
if (passphrase === null) {
|
|
335
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
336
|
+
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Capture previous mode (best-effort — info-only, do NOT block on read).
|
|
340
|
+
let prevMode = null;
|
|
341
|
+
try {
|
|
342
|
+
const readAdapter = new HyperliquidAdapter(undefined, isTestnet);
|
|
343
|
+
const { loadOws } = await import("../signer/ows-loader.js");
|
|
344
|
+
const ows = loadOws();
|
|
345
|
+
const wallet = ows.getWallet(masterName);
|
|
346
|
+
const evmAccount = wallet.accounts.find((a) => a.chainId.startsWith("eip155:"));
|
|
347
|
+
if (evmAccount) {
|
|
348
|
+
readAdapter.setAddress(evmAccount.address);
|
|
349
|
+
prevMode = await readAdapter._getAbstractionMode();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Best-effort previous-mode capture. SSOT Rule #2 forbids fallback
|
|
354
|
+
// for the primary action, but reading the prior mode is purely
|
|
355
|
+
// informational metadata — its absence does NOT mask the write.
|
|
356
|
+
prevMode = null;
|
|
357
|
+
}
|
|
358
|
+
const result = await runHlSetAbstractionFlow({
|
|
359
|
+
masterName,
|
|
360
|
+
passphrase: passphrase ?? "",
|
|
361
|
+
mode: newMode,
|
|
362
|
+
isTestnet,
|
|
363
|
+
});
|
|
364
|
+
if (useJson) {
|
|
365
|
+
return printJson(jsonOk({
|
|
366
|
+
mode: result.mode,
|
|
367
|
+
prevMode,
|
|
368
|
+
abstraction: result.abstraction,
|
|
369
|
+
hyperliquidChain: result.hyperliquidChain,
|
|
370
|
+
nonce: result.nonce,
|
|
371
|
+
user: result.userEvmAddress,
|
|
372
|
+
response: result.response,
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
console.log(chalk.green.bold("\n Hyperliquid account mode updated.\n"));
|
|
376
|
+
console.log(` User: ${chalk.gray(result.userEvmAddress)}`);
|
|
377
|
+
console.log(` Network: ${chalk.gray(result.hyperliquidChain)}`);
|
|
378
|
+
if (prevMode)
|
|
379
|
+
console.log(` Previous: ${chalk.gray(prevMode)}`);
|
|
380
|
+
console.log(` New mode: ${chalk.cyan(result.mode)} ${chalk.gray(`(${result.abstraction})`)}\n`);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
if (useJson) {
|
|
384
|
+
const e = err instanceof PerpError
|
|
385
|
+
? err.structured
|
|
386
|
+
: { code: "UNKNOWN", message: err instanceof Error ? err.message : String(err) };
|
|
387
|
+
console.error(JSON.stringify(jsonError(e.code, e.message)));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
239
394
|
}
|
package/dist/commands/wallet.js
CHANGED
|
@@ -613,29 +613,14 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
613
613
|
// Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter, ast → aster)
|
|
614
614
|
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
|
|
615
615
|
const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
|
|
616
|
-
// v0.12.4: Aster legacy HMAC API-key path is removed entirely. Plan v3.0
|
|
617
|
-
// de-wired the runtime/signing surface but left the wallet set entry
|
|
618
|
-
// accepting keys that no longer drive any signing path. Fail loudly
|
|
619
|
-
// (SSOT Rule #2) and point at the agent flow.
|
|
620
|
-
if (resolved === "aster") {
|
|
621
|
-
const msg = "'wallet set aster' is removed in v0.13. Use 'wallet agent approve aster' instead.";
|
|
622
|
-
const remediation = "perp wallet agent approve aster --master <wallet>";
|
|
623
|
-
if (isJson()) {
|
|
624
|
-
const { jsonError } = await import("../utils.js");
|
|
625
|
-
return printJson(jsonError("INVALID_PARAMS", msg, { details: { remediation } }));
|
|
626
|
-
}
|
|
627
|
-
console.error(chalk.red(`\n ${msg}`));
|
|
628
|
-
console.error(chalk.gray(` Run: ${remediation}\n`));
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
631
616
|
const info = EXCHANGE_ENV_MAP[resolved];
|
|
632
617
|
if (!info) {
|
|
633
618
|
if (isJson()) {
|
|
634
619
|
const { jsonError } = await import("../utils.js");
|
|
635
|
-
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter (or hl, pac, lt)
|
|
620
|
+
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast).`));
|
|
636
621
|
}
|
|
637
622
|
console.error(chalk.red(`\n Unknown exchange: ${exchange}`));
|
|
638
|
-
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter (or hl, pac, lt)
|
|
623
|
+
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter, aster (or hl, pac, lt, ast).\n`));
|
|
639
624
|
process.exit(1);
|
|
640
625
|
}
|
|
641
626
|
const { valid, address } = await validateKey(info.chain, key);
|
|
@@ -33,6 +33,12 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
33
33
|
get isTestnet(): boolean;
|
|
34
34
|
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
35
35
|
setSigner(signer: EvmSigner): void;
|
|
36
|
+
/**
|
|
37
|
+
* Set the user EVM address without attaching a signer. Used by read-only
|
|
38
|
+
* paths (e.g. `wallet manage account-mode` show branch) that need to query
|
|
39
|
+
* `/info` endpoints scoped to a master wallet but never sign.
|
|
40
|
+
*/
|
|
41
|
+
setAddress(address: string): void;
|
|
36
42
|
/** Tier 1: inject agent OWS wallet signer. */
|
|
37
43
|
setAgentSigner(meta: AgentMeta, signer: EvmSigner): void;
|
|
38
44
|
/** Bypass Tier 1 (agent) — use for --no-agent flag. */
|
|
@@ -64,6 +70,25 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
64
70
|
private ensureAddress;
|
|
65
71
|
/** Read-through cached clearinghouseState — returns cached data if fresh */
|
|
66
72
|
private _getClearinghouseState;
|
|
73
|
+
/**
|
|
74
|
+
* Read the user's account abstraction mode from HL.
|
|
75
|
+
*
|
|
76
|
+
* Returns the canonical mode used to branch collateral accounting:
|
|
77
|
+
* - "unified" → spot clearinghouseState is the truth source (USDC total
|
|
78
|
+
* is unified across spot + perp). app.hyperliquid.xyz default.
|
|
79
|
+
* - "standard" → perp clearinghouseState is the truth source. Required
|
|
80
|
+
* mode for builder fee accrual.
|
|
81
|
+
* - "portfolio" → multi-asset cross-collateral; for now treated like
|
|
82
|
+
* unified (spot truth). Refine when portfolio margin GA.
|
|
83
|
+
*
|
|
84
|
+
* Per SSOT Rule #2: an unrecognized venue response throws — no silent
|
|
85
|
+
* default. HIP-3 dex accounts (`this._dex` set) bypass the lookup since
|
|
86
|
+
* cross margin scopes per-DEX in standard semantics.
|
|
87
|
+
*
|
|
88
|
+
* Endpoint: `POST /info {"type":"userAbstraction","user":"0x..."}` →
|
|
89
|
+
* returns one of `"unifiedAccount" | "disabled" | "portfolioMargin"`.
|
|
90
|
+
*/
|
|
91
|
+
_getAbstractionMode(): Promise<"unified" | "standard" | "portfolio">;
|
|
67
92
|
/** Cached spot clearinghouse state — shared between getBalance() and HyperliquidSpotAdapter */
|
|
68
93
|
_getSpotClearinghouseState(): Promise<Record<string, unknown>>;
|
|
69
94
|
getBalance(): Promise<ExchangeBalance>;
|
|
@@ -61,6 +61,14 @@ export class HyperliquidAdapter {
|
|
|
61
61
|
this._evmSigner = signer;
|
|
62
62
|
this._address = signer.getAddress();
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Set the user EVM address without attaching a signer. Used by read-only
|
|
66
|
+
* paths (e.g. `wallet manage account-mode` show branch) that need to query
|
|
67
|
+
* `/info` endpoints scoped to a master wallet but never sign.
|
|
68
|
+
*/
|
|
69
|
+
setAddress(address) {
|
|
70
|
+
this._address = address;
|
|
71
|
+
}
|
|
64
72
|
/** Tier 1: inject agent OWS wallet signer. */
|
|
65
73
|
setAgentSigner(meta, signer) {
|
|
66
74
|
this._agentMeta = meta;
|
|
@@ -344,6 +352,40 @@ export class HyperliquidAdapter {
|
|
|
344
352
|
return state;
|
|
345
353
|
});
|
|
346
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Read the user's account abstraction mode from HL.
|
|
357
|
+
*
|
|
358
|
+
* Returns the canonical mode used to branch collateral accounting:
|
|
359
|
+
* - "unified" → spot clearinghouseState is the truth source (USDC total
|
|
360
|
+
* is unified across spot + perp). app.hyperliquid.xyz default.
|
|
361
|
+
* - "standard" → perp clearinghouseState is the truth source. Required
|
|
362
|
+
* mode for builder fee accrual.
|
|
363
|
+
* - "portfolio" → multi-asset cross-collateral; for now treated like
|
|
364
|
+
* unified (spot truth). Refine when portfolio margin GA.
|
|
365
|
+
*
|
|
366
|
+
* Per SSOT Rule #2: an unrecognized venue response throws — no silent
|
|
367
|
+
* default. HIP-3 dex accounts (`this._dex` set) bypass the lookup since
|
|
368
|
+
* cross margin scopes per-DEX in standard semantics.
|
|
369
|
+
*
|
|
370
|
+
* Endpoint: `POST /info {"type":"userAbstraction","user":"0x..."}` →
|
|
371
|
+
* returns one of `"unifiedAccount" | "disabled" | "portfolioMargin"`.
|
|
372
|
+
*/
|
|
373
|
+
async _getAbstractionMode() {
|
|
374
|
+
if (this._dex)
|
|
375
|
+
return "standard";
|
|
376
|
+
this.ensureAddress();
|
|
377
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
378
|
+
return withCache(`acct:hl:abstraction:${this._address}`, TTL_ACCOUNT, async () => {
|
|
379
|
+
const raw = (await this._infoPost({ type: "userAbstraction", user: this._address }));
|
|
380
|
+
if (raw === "unifiedAccount")
|
|
381
|
+
return "unified";
|
|
382
|
+
if (raw === "portfolioMargin")
|
|
383
|
+
return "portfolio";
|
|
384
|
+
if (raw === "disabled")
|
|
385
|
+
return "standard";
|
|
386
|
+
throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
|
|
387
|
+
});
|
|
388
|
+
}
|
|
347
389
|
/** Cached spot clearinghouse state — shared between getBalance() and HyperliquidSpotAdapter */
|
|
348
390
|
async _getSpotClearinghouseState() {
|
|
349
391
|
this.ensureAddress();
|
|
@@ -367,26 +409,24 @@ export class HyperliquidAdapter {
|
|
|
367
409
|
}
|
|
368
410
|
let equity;
|
|
369
411
|
let available;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
available = Number(s?.withdrawable ?? 0);
|
|
386
|
-
}
|
|
412
|
+
// Branch by user's account abstraction mode (Rule #2: no silent fallback).
|
|
413
|
+
// - "unified" / "portfolio": spot USDC total = true equity; perp margin
|
|
414
|
+
// is held against the same USDC pool, so adding both double-counts.
|
|
415
|
+
// - "standard": separate perp/spot accounting; perp clearinghouse is the
|
|
416
|
+
// only collateral source. Required mode for builder fee accrual.
|
|
417
|
+
// HIP-3 dex accounts (this._dex) always use standard semantics.
|
|
418
|
+
const mode = await this._getAbstractionMode();
|
|
419
|
+
if (mode === "unified" || mode === "portfolio") {
|
|
420
|
+
const spotState = await this._getSpotClearinghouseState();
|
|
421
|
+
const balances = (spotState?.balances ?? []);
|
|
422
|
+
const usdc = balances.find((b) => String(b.coin).startsWith("USDC"));
|
|
423
|
+
const spotTotal = Number(usdc?.total ?? 0);
|
|
424
|
+
const spotHold = Number(usdc?.hold ?? 0);
|
|
425
|
+
equity = spotTotal;
|
|
426
|
+
available = spotTotal - spotHold;
|
|
387
427
|
}
|
|
388
428
|
else {
|
|
389
|
-
//
|
|
429
|
+
// mode === "standard"
|
|
390
430
|
equity = Number(margin.accountValue ?? cross.accountValue ?? 0);
|
|
391
431
|
available = Number(s?.withdrawable ?? 0);
|
|
392
432
|
}
|
package/package.json
CHANGED
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
|
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
7
|
author: hypurrquant
|
|
8
|
-
version: "0.12.
|
|
8
|
+
version: "0.12.8"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# perp-cli Agent Guide
|
|
@@ -41,7 +41,7 @@ Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
perp --version 2>/dev/null # check if exists (must be >= 0.12.
|
|
44
|
+
perp --version 2>/dev/null # check if exists (must be >= 0.12.8)
|
|
45
45
|
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
46
46
|
```
|
|
47
47
|
|