perp-cli 0.12.9 → 0.12.11
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/manage.js +1 -2
- package/dist/commands/wallet.js +9 -2
- package/dist/config.js +6 -5
- package/dist/exchanges/aster-typed-data.d.ts +17 -4
- package/dist/exchanges/aster-typed-data.js +17 -4
- package/dist/exchanges/aster.d.ts +42 -4
- package/dist/exchanges/aster.js +61 -26
- package/dist/exchanges/hyperliquid.js +28 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +2 -2
package/dist/commands/manage.js
CHANGED
|
@@ -239,8 +239,7 @@ export function registerWalletManageCommands(parent, getAdapter, isJson, getPaci
|
|
|
239
239
|
// Legacy `manage setup-api-key` removed in v0.12 (Phase 2d) — superseded by
|
|
240
240
|
// the unified `wallet agent approve lighter` flow. The agent flow provides
|
|
241
241
|
// 3-tier signer routing, expiry tracking, free-slot picking, and AgentMeta
|
|
242
|
-
// persistence.
|
|
243
|
-
// env-key fallback for users who haven't run the unified command yet.
|
|
242
|
+
// persistence.
|
|
244
243
|
// ── Hyperliquid account abstraction mode ────────────────────────────────
|
|
245
244
|
// Read side: HyperliquidAdapter._getAbstractionMode() (added v0.12.7).
|
|
246
245
|
// Write side: HL `userSetAbstraction` action — master EVM EIP-712 signature.
|
package/dist/commands/wallet.js
CHANGED
|
@@ -1270,12 +1270,19 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
1270
1270
|
.option("--policy <ids>", "Comma-separated policy IDs to enforce")
|
|
1271
1271
|
.option("--expires <iso>", "Key expiry (ISO-8601)")
|
|
1272
1272
|
.option("-p, --passphrase <pass>", "Vault passphrase", "")
|
|
1273
|
-
.action(async (opts) => {
|
|
1273
|
+
.action(async (opts, command) => {
|
|
1274
1274
|
try {
|
|
1275
1275
|
const o = loadOws();
|
|
1276
1276
|
const walletIds = opts.wallets.split(",").map(s => s.trim());
|
|
1277
1277
|
const policyIds = opts.policy ? opts.policy.split(",").map(s => s.trim()) : [];
|
|
1278
|
-
|
|
1278
|
+
// optsWithGlobals() merges parent + subcommand options, so --passphrase
|
|
1279
|
+
// works whether placed on `perp --passphrase X wallet key create` (parent)
|
|
1280
|
+
// or `perp wallet key create --passphrase X` (subcommand). Without this
|
|
1281
|
+
// merge, Commander v13 silently shadows the subcommand flag with the
|
|
1282
|
+
// parent flag and opts.passphrase becomes the empty-string default.
|
|
1283
|
+
const merged = command.optsWithGlobals();
|
|
1284
|
+
const passphrase = merged.passphrase ?? opts.passphrase;
|
|
1285
|
+
const result = o.createApiKey(opts.name, walletIds, policyIds, passphrase, opts.expires);
|
|
1279
1286
|
if (isJson())
|
|
1280
1287
|
return printJson(jsonOk({ id: result.id, name: result.name, token: result.token, wallets: walletIds, policies: policyIds, expires: opts.expires ?? null }));
|
|
1281
1288
|
console.log(chalk.green.bold("\n OWS API Key Created\n"));
|
package/dist/config.js
CHANGED
|
@@ -74,15 +74,16 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
74
74
|
// settings module not available, skip
|
|
75
75
|
}
|
|
76
76
|
// 3. Exchange-specific env vars
|
|
77
|
-
// v0.12.4
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// v0.12.4 retired the legacy HMAC ASTER_API_KEY/SECRET path.
|
|
78
|
+
// v0.12.6 restored Aster as a normal EVM exchange in EXCHANGE_ENV_MAP
|
|
79
|
+
// (setup wizard / wallet show), but missed this map — leaving Tier 3
|
|
80
|
+
// env-PK loading broken. v0.12.10 restores ASTER_PRIVATE_KEY here so
|
|
81
|
+
// `tryLoadPrivateKey("aster")` finds the env value, mirroring HL/LT.
|
|
81
82
|
const envMap = {
|
|
82
83
|
pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
|
|
83
84
|
hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
|
|
84
85
|
lighter: ["LIGHTER_PRIVATE_KEY"],
|
|
85
|
-
aster: [],
|
|
86
|
+
aster: ["ASTER_PRIVATE_KEY"],
|
|
86
87
|
};
|
|
87
88
|
for (const envVar of envMap[exchange]) {
|
|
88
89
|
if (process.env[envVar])
|
|
@@ -25,6 +25,15 @@ export declare const ASTER_DOMAIN_B: {
|
|
|
25
25
|
readonly version: "1";
|
|
26
26
|
readonly verifyingContract: "0x0000000000000000000000000000000000000000";
|
|
27
27
|
};
|
|
28
|
+
/** Domain B (testnet): chainId=714 per Aster V3 testnet docs. */
|
|
29
|
+
export declare const ASTER_DOMAIN_B_TESTNET: {
|
|
30
|
+
readonly chainId: 714;
|
|
31
|
+
readonly name: "AsterSignTransaction";
|
|
32
|
+
readonly version: "1";
|
|
33
|
+
readonly verifyingContract: "0x0000000000000000000000000000000000000000";
|
|
34
|
+
};
|
|
35
|
+
/** Returns the Domain B object for the requested network (testnet branches chainId). */
|
|
36
|
+
export declare function getAsterDomainB(testnet: boolean): typeof ASTER_DOMAIN_B | typeof ASTER_DOMAIN_B_TESTNET;
|
|
28
37
|
/** EIP-712 types for Domain B order signing — single msg string (URL-encoded query string). */
|
|
29
38
|
export declare const ORDER_TYPES: {
|
|
30
39
|
readonly Message: readonly [{
|
|
@@ -113,11 +122,15 @@ export declare function buildOrderQueryStringMsg(orderParams: Record<string, str
|
|
|
113
122
|
/**
|
|
114
123
|
* Build the EIP-712 typed-data payload for agent-signed order requests.
|
|
115
124
|
*
|
|
116
|
-
* Domain B (chainId=1666), primaryType="Message",
|
|
117
|
-
* the URL-encoded query string produced by
|
|
125
|
+
* Domain B (chainId=1666 mainnet / 714 testnet), primaryType="Message",
|
|
126
|
+
* single `msg` field containing the URL-encoded query string produced by
|
|
127
|
+
* buildOrderQueryStringMsg.
|
|
128
|
+
*
|
|
129
|
+
* @param testnet When true, returns Domain B with chainId=714 per Aster V3
|
|
130
|
+
* testnet docs. Defaults to false (mainnet).
|
|
118
131
|
*/
|
|
119
|
-
export declare function buildOrderTypedData(orderParams: Record<string, string | number | boolean
|
|
120
|
-
domain: typeof ASTER_DOMAIN_B;
|
|
132
|
+
export declare function buildOrderTypedData(orderParams: Record<string, string | number | boolean>, testnet?: boolean): {
|
|
133
|
+
domain: typeof ASTER_DOMAIN_B | typeof ASTER_DOMAIN_B_TESTNET;
|
|
121
134
|
types: typeof ORDER_TYPES;
|
|
122
135
|
primaryType: "Message";
|
|
123
136
|
message: {
|
|
@@ -24,6 +24,15 @@ export const ASTER_DOMAIN_B = {
|
|
|
24
24
|
...ASTER_DOMAIN_A,
|
|
25
25
|
chainId: 1666,
|
|
26
26
|
};
|
|
27
|
+
/** Domain B (testnet): chainId=714 per Aster V3 testnet docs. */
|
|
28
|
+
export const ASTER_DOMAIN_B_TESTNET = {
|
|
29
|
+
...ASTER_DOMAIN_A,
|
|
30
|
+
chainId: 714,
|
|
31
|
+
};
|
|
32
|
+
/** Returns the Domain B object for the requested network (testnet branches chainId). */
|
|
33
|
+
export function getAsterDomainB(testnet) {
|
|
34
|
+
return testnet ? ASTER_DOMAIN_B_TESTNET : ASTER_DOMAIN_B;
|
|
35
|
+
}
|
|
27
36
|
/** EIP-712 types for Domain B order signing — single msg string (URL-encoded query string). */
|
|
28
37
|
export const ORDER_TYPES = {
|
|
29
38
|
Message: [{ name: "msg", type: "string" }],
|
|
@@ -136,13 +145,17 @@ export function buildOrderQueryStringMsg(orderParams) {
|
|
|
136
145
|
/**
|
|
137
146
|
* Build the EIP-712 typed-data payload for agent-signed order requests.
|
|
138
147
|
*
|
|
139
|
-
* Domain B (chainId=1666), primaryType="Message",
|
|
140
|
-
* the URL-encoded query string produced by
|
|
148
|
+
* Domain B (chainId=1666 mainnet / 714 testnet), primaryType="Message",
|
|
149
|
+
* single `msg` field containing the URL-encoded query string produced by
|
|
150
|
+
* buildOrderQueryStringMsg.
|
|
151
|
+
*
|
|
152
|
+
* @param testnet When true, returns Domain B with chainId=714 per Aster V3
|
|
153
|
+
* testnet docs. Defaults to false (mainnet).
|
|
141
154
|
*/
|
|
142
|
-
export function buildOrderTypedData(orderParams) {
|
|
155
|
+
export function buildOrderTypedData(orderParams, testnet = false) {
|
|
143
156
|
const msg = buildOrderQueryStringMsg(orderParams);
|
|
144
157
|
return {
|
|
145
|
-
domain:
|
|
158
|
+
domain: getAsterDomainB(testnet),
|
|
146
159
|
types: ORDER_TYPES,
|
|
147
160
|
primaryType: "Message",
|
|
148
161
|
message: { msg },
|
|
@@ -15,6 +15,14 @@ import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrd
|
|
|
15
15
|
import type { EvmSigner } from "../signer/interface.js";
|
|
16
16
|
import type { AgentMeta } from "../settings.js";
|
|
17
17
|
import type { AgentSigningStrategy } from "../agent-wallet/signing-strategy.js";
|
|
18
|
+
type ResolvedSigner = {
|
|
19
|
+
tier: "agent" | "master" | "pk";
|
|
20
|
+
signer: EvmSigner | AgentSigningStrategy;
|
|
21
|
+
/** EVM address that will appear in the `signer` field of Aster requests */
|
|
22
|
+
signerAddress: string;
|
|
23
|
+
/** EVM address of the owner/master — used as `user` field in Aster requests */
|
|
24
|
+
userAddress: string;
|
|
25
|
+
};
|
|
18
26
|
export declare class AsterAdapter implements ExchangeAdapter {
|
|
19
27
|
readonly name = "aster";
|
|
20
28
|
readonly chain = "bnb";
|
|
@@ -88,7 +96,13 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
88
96
|
assetId?: number;
|
|
89
97
|
routeType?: number;
|
|
90
98
|
}): Promise<unknown>;
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the active signer per the three-tier policy.
|
|
101
|
+
*
|
|
102
|
+
* Exposed (leading underscore) for unit-test access only — do not call
|
|
103
|
+
* from production code outside this class.
|
|
104
|
+
*/
|
|
105
|
+
_resolveSigner(): ResolvedSigner;
|
|
92
106
|
private _nextNonce;
|
|
93
107
|
/**
|
|
94
108
|
* Build the canonical signed-request URL per Aster v3 spec.
|
|
@@ -102,24 +116,48 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
102
116
|
* signed dict and the URL query string must be byte-identical except for
|
|
103
117
|
* the appended signature.
|
|
104
118
|
*
|
|
119
|
+
* Both `user` and `signer` are ALWAYS emitted as distinct query fields,
|
|
120
|
+
* even when the values are identical (Tier 2/3 self-signing case where
|
|
121
|
+
* master signs for itself). Aster's authority server checks both fields.
|
|
122
|
+
*
|
|
105
123
|
* `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
|
|
106
124
|
* scheme; including it breaks signature verification on every call.
|
|
125
|
+
*
|
|
126
|
+
* Exposed (with leading underscore) for unit-test access only — do not call
|
|
127
|
+
* from production code outside this class.
|
|
107
128
|
*/
|
|
108
|
-
|
|
129
|
+
_buildSignedQueryString(params: Record<string, string | number | boolean>, resolved: ResolvedSigner): Promise<string>;
|
|
109
130
|
/**
|
|
110
131
|
* Sign and POST to a path using EIP-712 Domain B.
|
|
111
132
|
*/
|
|
112
133
|
private _signedPostEip712;
|
|
113
134
|
/**
|
|
114
135
|
* Sign and GET from a path using EIP-712 Domain B.
|
|
136
|
+
*
|
|
137
|
+
* Unlike public GETs, signed GETs route through _handleAsterResponse so
|
|
138
|
+
* venue JSON error envelopes ({code, msg}) are not silently treated as
|
|
139
|
+
* success (SSOT Rule #2).
|
|
115
140
|
*/
|
|
116
141
|
private _signedGetEip712;
|
|
117
142
|
/**
|
|
118
143
|
* Sign and DELETE from a path using EIP-712 Domain B.
|
|
119
144
|
*/
|
|
120
145
|
private _signedDeleteEip712;
|
|
121
|
-
|
|
122
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Unified response validator for ALL signed Aster requests.
|
|
148
|
+
*
|
|
149
|
+
* Validates BOTH HTTP status AND the venue JSON error envelope. Aster's
|
|
150
|
+
* API commonly returns HTTP 200 + `{code, msg: "Signature check failed"}`
|
|
151
|
+
* for signing or auth failures — checking only HTTP status would silently
|
|
152
|
+
* accept these as success and cause callers (e.g., getBalance) to cache
|
|
153
|
+
* zero balances or empty arrays. SSOT Rule #2: failures throw with
|
|
154
|
+
* remediation; never substitute a default.
|
|
155
|
+
*
|
|
156
|
+
* Used by signed POST/GET/DELETE. Public unsigned GETs use _handleResponse.
|
|
157
|
+
*/
|
|
158
|
+
private _handleAsterResponse;
|
|
159
|
+
/** Handle 429 rate limit with retry — used by public unsigned GET multi-attempt loops */
|
|
123
160
|
private _handleResponse;
|
|
124
161
|
private _publicGet;
|
|
125
162
|
}
|
|
163
|
+
export {};
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -501,6 +501,12 @@ export class AsterAdapter {
|
|
|
501
501
|
throw new Error("Aster withdrawal requires the spot API (not available in futures mode). Use the Aster web UI to withdraw.");
|
|
502
502
|
}
|
|
503
503
|
// ── Private: signer resolution ────────────────────────────────────────────
|
|
504
|
+
/**
|
|
505
|
+
* Resolve the active signer per the three-tier policy.
|
|
506
|
+
*
|
|
507
|
+
* Exposed (leading underscore) for unit-test access only — do not call
|
|
508
|
+
* from production code outside this class.
|
|
509
|
+
*/
|
|
504
510
|
_resolveSigner() {
|
|
505
511
|
// Tier 1: agent (when registered, not expired, --no-agent NOT set)
|
|
506
512
|
if (!this._useNoAgent && this._agentSigner && this._agentMeta) {
|
|
@@ -522,12 +528,15 @@ export class AsterAdapter {
|
|
|
522
528
|
};
|
|
523
529
|
}
|
|
524
530
|
}
|
|
525
|
-
// Tier 2: OWS master
|
|
531
|
+
// Tier 2: OWS master — master self-signs, so user==signer per V3 spec.
|
|
532
|
+
// The query string MUST still emit `user=` AND `signer=` as distinct
|
|
533
|
+
// fields even when the values are identical (Aster authority verifies
|
|
534
|
+
// both keys); see _buildSignedQueryString.
|
|
526
535
|
if (this._masterSigner) {
|
|
527
536
|
const addr = this._masterSigner.getAddress();
|
|
528
537
|
return { tier: "master", signer: this._masterSigner, signerAddress: addr, userAddress: addr };
|
|
529
538
|
}
|
|
530
|
-
// Tier 3: PK direct
|
|
539
|
+
// Tier 3: PK direct — same self-signing model as Tier 2.
|
|
531
540
|
if (this._pkSigner) {
|
|
532
541
|
const addr = this._pkSigner.getAddress();
|
|
533
542
|
return { tier: "pk", signer: this._pkSigner, signerAddress: addr, userAddress: addr };
|
|
@@ -555,8 +564,15 @@ export class AsterAdapter {
|
|
|
555
564
|
* signed dict and the URL query string must be byte-identical except for
|
|
556
565
|
* the appended signature.
|
|
557
566
|
*
|
|
567
|
+
* Both `user` and `signer` are ALWAYS emitted as distinct query fields,
|
|
568
|
+
* even when the values are identical (Tier 2/3 self-signing case where
|
|
569
|
+
* master signs for itself). Aster's authority server checks both fields.
|
|
570
|
+
*
|
|
558
571
|
* `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
|
|
559
572
|
* scheme; including it breaks signature verification on every call.
|
|
573
|
+
*
|
|
574
|
+
* Exposed (with leading underscore) for unit-test access only — do not call
|
|
575
|
+
* from production code outside this class.
|
|
560
576
|
*/
|
|
561
577
|
async _buildSignedQueryString(params, resolved) {
|
|
562
578
|
// Insertion order (matches Python reference: nonce, user, signer last).
|
|
@@ -566,7 +582,7 @@ export class AsterAdapter {
|
|
|
566
582
|
user: resolved.userAddress,
|
|
567
583
|
signer: resolved.signerAddress,
|
|
568
584
|
};
|
|
569
|
-
const typed = buildOrderTypedData(fullParams);
|
|
585
|
+
const typed = buildOrderTypedData(fullParams, this._testnet);
|
|
570
586
|
// Sign via the appropriate signer type
|
|
571
587
|
const sigRaw = await resolved.signer.signTypedData(typed.domain, typed.types, typed.message);
|
|
572
588
|
// Normalize: AgentSigningStrategy returns { signature, r, s, v }; EvmSigner returns string
|
|
@@ -586,21 +602,20 @@ export class AsterAdapter {
|
|
|
586
602
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
587
603
|
body: "",
|
|
588
604
|
});
|
|
589
|
-
return this.
|
|
605
|
+
return this._handleAsterResponse(res, "POST", path);
|
|
590
606
|
}
|
|
591
607
|
/**
|
|
592
608
|
* Sign and GET from a path using EIP-712 Domain B.
|
|
609
|
+
*
|
|
610
|
+
* Unlike public GETs, signed GETs route through _handleAsterResponse so
|
|
611
|
+
* venue JSON error envelopes ({code, msg}) are not silently treated as
|
|
612
|
+
* success (SSOT Rule #2).
|
|
593
613
|
*/
|
|
594
614
|
async _signedGetEip712(path, params, resolved) {
|
|
595
615
|
const qs = await this._buildSignedQueryString(params, resolved);
|
|
596
616
|
const url = `${this._baseUrl}${path}?${qs}`;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const result = await this._handleResponse(res, "GET", path, attempt);
|
|
600
|
-
if (result !== null)
|
|
601
|
-
return result;
|
|
602
|
-
}
|
|
603
|
-
throw new Error(`GET ${path} failed: max retries exceeded`);
|
|
617
|
+
const res = await fetch(url);
|
|
618
|
+
return this._handleAsterResponse(res, "GET", path);
|
|
604
619
|
}
|
|
605
620
|
/**
|
|
606
621
|
* Sign and DELETE from a path using EIP-712 Domain B.
|
|
@@ -608,16 +623,23 @@ export class AsterAdapter {
|
|
|
608
623
|
async _signedDeleteEip712(path, params, resolved) {
|
|
609
624
|
const qs = await this._buildSignedQueryString(params, resolved);
|
|
610
625
|
const url = `${this._baseUrl}${path}?${qs}`;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const result = await this._handleResponse(res, "DELETE", path, attempt);
|
|
614
|
-
if (result !== null)
|
|
615
|
-
return result;
|
|
616
|
-
}
|
|
617
|
-
throw new Error(`DELETE ${path} failed: max retries exceeded`);
|
|
626
|
+
const res = await fetch(url, { method: "DELETE" });
|
|
627
|
+
return this._handleAsterResponse(res, "DELETE", path);
|
|
618
628
|
}
|
|
619
629
|
// ── Private: HTTP response handling ──────────────────────────────────────
|
|
620
|
-
|
|
630
|
+
/**
|
|
631
|
+
* Unified response validator for ALL signed Aster requests.
|
|
632
|
+
*
|
|
633
|
+
* Validates BOTH HTTP status AND the venue JSON error envelope. Aster's
|
|
634
|
+
* API commonly returns HTTP 200 + `{code, msg: "Signature check failed"}`
|
|
635
|
+
* for signing or auth failures — checking only HTTP status would silently
|
|
636
|
+
* accept these as success and cause callers (e.g., getBalance) to cache
|
|
637
|
+
* zero balances or empty arrays. SSOT Rule #2: failures throw with
|
|
638
|
+
* remediation; never substitute a default.
|
|
639
|
+
*
|
|
640
|
+
* Used by signed POST/GET/DELETE. Public unsigned GETs use _handleResponse.
|
|
641
|
+
*/
|
|
642
|
+
async _handleAsterResponse(res, method, path) {
|
|
621
643
|
if (res.status === 429) {
|
|
622
644
|
const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
|
|
623
645
|
await new Promise(r => setTimeout(r, Math.min(retryAfter * 1000, 30000)));
|
|
@@ -628,15 +650,28 @@ export class AsterAdapter {
|
|
|
628
650
|
const s = classifyError(new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 200)}`), "aster");
|
|
629
651
|
throw new PerpError(s.code, s.message, { exchange: s.exchange });
|
|
630
652
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
653
|
+
// Parse JSON, then validate venue error envelope. Aster success returns
|
|
654
|
+
// either an object whose `code` is "000000"/200 (or absent) or an array
|
|
655
|
+
// (e.g., GET /fapi/v3/openOrders, GET /fapi/v3/userTrades).
|
|
656
|
+
const text = await res.text();
|
|
657
|
+
let parsed;
|
|
658
|
+
try {
|
|
659
|
+
parsed = JSON.parse(text);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
throw new PerpError("EXCHANGE_ERROR", `${method} ${path}: malformed JSON: ${text.slice(0, 200)}`, { exchange: "aster" });
|
|
663
|
+
}
|
|
664
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
665
|
+
const json = parsed;
|
|
666
|
+
if (json.code !== undefined && json.code !== "000000" && json.code !== 200 && json.code !== 0) {
|
|
667
|
+
const rawMsg = typeof json.msg === "string" ? json.msg : JSON.stringify(json);
|
|
668
|
+
const s = classifyError(new Error(rawMsg), "aster");
|
|
669
|
+
throw new PerpError(s.code, s.message, { exchange: s.exchange });
|
|
670
|
+
}
|
|
636
671
|
}
|
|
637
|
-
return
|
|
672
|
+
return parsed;
|
|
638
673
|
}
|
|
639
|
-
/** Handle 429 rate limit with retry — used by GET
|
|
674
|
+
/** Handle 429 rate limit with retry — used by public unsigned GET multi-attempt loops */
|
|
640
675
|
async _handleResponse(res, method, path, attempt = 0) {
|
|
641
676
|
if (res.status === 429) {
|
|
642
677
|
if (attempt >= 2)
|
|
@@ -392,6 +392,12 @@ export class HyperliquidAdapter {
|
|
|
392
392
|
// via `wallet manage account-mode unified`.
|
|
393
393
|
if (raw === "default")
|
|
394
394
|
return "standard";
|
|
395
|
+
// "dexAbstraction" is HL's legacy fourth mode, slated for deprecation
|
|
396
|
+
// but still returned by the venue for some users. Per HL docs the spot
|
|
397
|
+
// pool acts as collateral (same accounting semantics as unified), so
|
|
398
|
+
// route through the unified branch rather than throwing.
|
|
399
|
+
if (raw === "dexAbstraction")
|
|
400
|
+
return "unified";
|
|
395
401
|
throw new PerpError("INVALID_PARAMS", `UNKNOWN_ACCOUNT_MODE: HL returned unrecognized userAbstraction mode: ${JSON.stringify(raw)}`, { remediation: "Check app.hyperliquid.xyz Settings → Account Mode" });
|
|
396
402
|
});
|
|
397
403
|
}
|
|
@@ -424,6 +430,12 @@ export class HyperliquidAdapter {
|
|
|
424
430
|
// - "standard": separate perp/spot accounting; perp clearinghouse is the
|
|
425
431
|
// only collateral source. Required mode for builder fee accrual.
|
|
426
432
|
// HIP-3 dex accounts (this._dex) always use standard semantics.
|
|
433
|
+
//
|
|
434
|
+
// Portfolio-margin mode: per HL portfolio-margin docs, eligible collateral
|
|
435
|
+
// includes HYPE, BTC, USDH, USDC. We currently use USDC-only as a minimum
|
|
436
|
+
// viable baseline (mark-priced multi-asset accounting requires fetching
|
|
437
|
+
// spot mark prices and is deferred to a follow-up). Emit a stderr note so
|
|
438
|
+
// users see the breakdown when non-USDC collateral is present.
|
|
427
439
|
const mode = await this._getAbstractionMode();
|
|
428
440
|
if (mode === "unified" || mode === "portfolio") {
|
|
429
441
|
const spotState = await this._getSpotClearinghouseState();
|
|
@@ -433,6 +445,22 @@ export class HyperliquidAdapter {
|
|
|
433
445
|
const spotHold = Number(usdc?.hold ?? 0);
|
|
434
446
|
equity = spotTotal;
|
|
435
447
|
available = spotTotal - spotHold;
|
|
448
|
+
if (mode === "portfolio") {
|
|
449
|
+
const PORTFOLIO_COLLATERAL = ["HYPE", "BTC", "USDH"];
|
|
450
|
+
const nonUsdc = balances.filter((b) => {
|
|
451
|
+
const coin = String(b.coin);
|
|
452
|
+
if (coin.startsWith("USDC"))
|
|
453
|
+
return false;
|
|
454
|
+
if (Number(b.total ?? 0) <= 0)
|
|
455
|
+
return false;
|
|
456
|
+
return PORTFOLIO_COLLATERAL.some((c) => coin === c || coin.startsWith(`${c}-`));
|
|
457
|
+
});
|
|
458
|
+
if (nonUsdc.length > 0) {
|
|
459
|
+
const summary = nonUsdc.map((b) => `${b.coin}=${b.total}`).join(", ");
|
|
460
|
+
process.stderr.write(`[hl] portfolio mode: non-USDC collateral present but not counted in equity (${summary}). ` +
|
|
461
|
+
`Equity reflects USDC only; full multi-asset accounting pending.\n`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
436
464
|
}
|
|
437
465
|
else {
|
|
438
466
|
// mode === "standard"
|
package/dist/index.js
CHANGED
|
@@ -587,7 +587,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
587
587
|
const settings = loadSettings();
|
|
588
588
|
const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
|
|
589
589
|
process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
|
|
590
|
-
process.env.LIGHTER_PRIVATE_KEY);
|
|
590
|
+
process.env.LIGHTER_PRIVATE_KEY || process.env.ASTER_PRIVATE_KEY);
|
|
591
591
|
const hasOwsWallet = !!settings.owsActiveWallet;
|
|
592
592
|
// ── ASCII banner ──
|
|
593
593
|
const banner = [
|
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.11"
|
|
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.11)
|
|
45
45
|
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
46
46
|
```
|
|
47
47
|
|