perp-cli 0.12.11 → 0.12.12

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.
@@ -271,7 +271,7 @@ export function registerAlertCommands(program, isJson) {
271
271
  // Pass through env vars for keys
272
272
  // L2 agent key + indices now live in ~/.perp keystore + settings.agents (SSOT Rule #3),
273
273
  // not env. Only Tier 3 master PKs need passthrough.
274
- const envKeys = ["LIGHTER_PRIVATE_KEY", "HL_PRIVATE_KEY", "PACIFICA_PRIVATE_KEY", "PRIVATE_KEY"];
274
+ const envKeys = ["LIGHTER_PRIVATE_KEY", "HL_PRIVATE_KEY", "PACIFICA_PRIVATE_KEY", "ASTER_PRIVATE_KEY", "PRIVATE_KEY"];
275
275
  const envStr = envKeys.filter(k => process.env[k]).map(k => `${k}='${process.env[k]}'`).join(" ");
276
276
  const cmd = `${envStr} ${nodeCmd} ${cliPath} ${args}`;
277
277
  execSync(`tmux new-session -d -s ${session} '${cmd.replace(/'/g, "'\\''")}'`);
@@ -1,10 +1,17 @@
1
1
  /**
2
- * Aster DEX adapter — EIP-712 three-tier signer routing (v3.2).
2
+ * Aster DEX adapter — EIP-712 agent-only signer routing (v3.3).
3
3
  *
4
- * Signer priority:
5
- * Tier 1 — Agent OWS wallet (registered + not expired + no --no-agent)
6
- * Tier 2 — OWS master wallet (--ows / owsActiveWallet)
7
- * Tier 3 — PK direct (--private-key / PRIVATE_KEY / ASTER_PRIVATE_KEY)
4
+ * Signer policy:
5
+ * Tier 1 — Agent OWS wallet (registered + not expired + no --no-agent) — REQUIRED
6
+ * Tier 2 — OWS master — NOT_SUPPORTED (venue rejects master self-signing)
7
+ * Tier 3 — PK direct — NOT_SUPPORTED (venue rejects master self-signing)
8
+ *
9
+ * Aster V3 spec requires `signer` to be a registered API_WALLET (agent). The
10
+ * master wallet is never a valid signer — venue returns
11
+ * `code:-1000 msg:"Signature check failed"` when user==signer==master, even
12
+ * though the EIP-712 envelope is well-formed. Master/PK signers are still
13
+ * accepted via setMasterSigner/setPkSigner (used during approveAgent flow),
14
+ * but signed read/trade requests must use Tier 1.
8
15
  *
9
16
  * HMAC paths fully removed (see Step 3 plan — all Binance-compat HMAC code deleted).
10
17
  *
@@ -116,9 +123,9 @@ export declare class AsterAdapter implements ExchangeAdapter {
116
123
  * signed dict and the URL query string must be byte-identical except for
117
124
  * the appended signature.
118
125
  *
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.
126
+ * Both `user` and `signer` are ALWAYS emitted as distinct query fields.
127
+ * In production (Tier 1 only), `user` is the master and `signer` is the
128
+ * registered agent address. Aster's authority server checks both fields.
122
129
  *
123
130
  * `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
124
131
  * scheme; including it breaks signature verification on every call.
@@ -137,12 +144,20 @@ export declare class AsterAdapter implements ExchangeAdapter {
137
144
  * Unlike public GETs, signed GETs route through _handleAsterResponse so
138
145
  * venue JSON error envelopes ({code, msg}) are not silently treated as
139
146
  * success (SSOT Rule #2).
147
+ *
148
+ * Retries up to 3 attempts with exponential backoff (2s/4s/8s) on HTTP 429
149
+ * rate-limit. Each retry rebuilds a fresh signed query string (new nonce
150
+ * + signature) — Aster reuses recent nonces strictly, so reusing a stale
151
+ * one would itself be rejected. Non-429 venue errors throw immediately.
140
152
  */
141
153
  private _signedGetEip712;
142
154
  /**
143
155
  * Sign and DELETE from a path using EIP-712 Domain B.
156
+ * Same retry behavior as _signedGetEip712.
144
157
  */
145
158
  private _signedDeleteEip712;
159
+ /** Shared GET/DELETE retry loop with fresh-nonce per attempt. */
160
+ private _signedRequestWithRetry;
146
161
  /**
147
162
  * Unified response validator for ALL signed Aster requests.
148
163
  *
@@ -1,10 +1,17 @@
1
1
  /**
2
- * Aster DEX adapter — EIP-712 three-tier signer routing (v3.2).
2
+ * Aster DEX adapter — EIP-712 agent-only signer routing (v3.3).
3
3
  *
4
- * Signer priority:
5
- * Tier 1 — Agent OWS wallet (registered + not expired + no --no-agent)
6
- * Tier 2 — OWS master wallet (--ows / owsActiveWallet)
7
- * Tier 3 — PK direct (--private-key / PRIVATE_KEY / ASTER_PRIVATE_KEY)
4
+ * Signer policy:
5
+ * Tier 1 — Agent OWS wallet (registered + not expired + no --no-agent) — REQUIRED
6
+ * Tier 2 — OWS master — NOT_SUPPORTED (venue rejects master self-signing)
7
+ * Tier 3 — PK direct — NOT_SUPPORTED (venue rejects master self-signing)
8
+ *
9
+ * Aster V3 spec requires `signer` to be a registered API_WALLET (agent). The
10
+ * master wallet is never a valid signer — venue returns
11
+ * `code:-1000 msg:"Signature check failed"` when user==signer==master, even
12
+ * though the EIP-712 envelope is well-formed. Master/PK signers are still
13
+ * accepted via setMasterSigner/setPkSigner (used during approveAgent flow),
14
+ * but signed read/trade requests must use Tier 1.
8
15
  *
9
16
  * HMAC paths fully removed (see Step 3 plan — all Binance-compat HMAC code deleted).
10
17
  *
@@ -528,18 +535,19 @@ export class AsterAdapter {
528
535
  };
529
536
  }
530
537
  }
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.
535
- if (this._masterSigner) {
536
- const addr = this._masterSigner.getAddress();
537
- return { tier: "master", signer: this._masterSigner, signerAddress: addr, userAddress: addr };
538
- }
539
- // Tier 3: PK direct — same self-signing model as Tier 2.
540
- if (this._pkSigner) {
541
- const addr = this._pkSigner.getAddress();
542
- return { tier: "pk", signer: this._pkSigner, signerAddress: addr, userAddress: addr };
538
+ // Tier 2/3: master self-signing is not supported by the venue. Per Aster
539
+ // V3 spec the `signer` field MUST be a registered API_WALLET (agent);
540
+ // sending `user==signer==master` is rejected with
541
+ // `code:-1000 msg:"Signature check failed"` even though the EIP-712
542
+ // envelope is well-formed. SSOT Rule #2: throw with remediation rather
543
+ // than silently dispatching a request the venue will reject.
544
+ //
545
+ // Reference: HypurrQuant_FE AsterPerpAdapter.ts throws if agent isn't
546
+ // configured; never attempts master self-signing.
547
+ if (this._masterSigner || this._pkSigner) {
548
+ throw new PerpError("NOT_IMPLEMENTED", "Aster requires a registered agent — master self-signing is not supported by venue", {
549
+ remediation: "perp wallet agent approve aster --master <wallet>",
550
+ });
543
551
  }
544
552
  // No signer at any tier
545
553
  throw new PerpError("NO_SIGNER_AVAILABLE", "No signing path configured for Aster", {
@@ -564,9 +572,9 @@ export class AsterAdapter {
564
572
  * signed dict and the URL query string must be byte-identical except for
565
573
  * the appended signature.
566
574
  *
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.
575
+ * Both `user` and `signer` are ALWAYS emitted as distinct query fields.
576
+ * In production (Tier 1 only), `user` is the master and `signer` is the
577
+ * registered agent address. Aster's authority server checks both fields.
570
578
  *
571
579
  * `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
572
580
  * scheme; including it breaks signature verification on every call.
@@ -610,21 +618,53 @@ export class AsterAdapter {
610
618
  * Unlike public GETs, signed GETs route through _handleAsterResponse so
611
619
  * venue JSON error envelopes ({code, msg}) are not silently treated as
612
620
  * success (SSOT Rule #2).
621
+ *
622
+ * Retries up to 3 attempts with exponential backoff (2s/4s/8s) on HTTP 429
623
+ * rate-limit. Each retry rebuilds a fresh signed query string (new nonce
624
+ * + signature) — Aster reuses recent nonces strictly, so reusing a stale
625
+ * one would itself be rejected. Non-429 venue errors throw immediately.
613
626
  */
614
627
  async _signedGetEip712(path, params, resolved) {
615
- const qs = await this._buildSignedQueryString(params, resolved);
616
- const url = `${this._baseUrl}${path}?${qs}`;
617
- const res = await fetch(url);
618
- return this._handleAsterResponse(res, "GET", path);
628
+ return this._signedRequestWithRetry("GET", path, params, resolved);
619
629
  }
620
630
  /**
621
631
  * Sign and DELETE from a path using EIP-712 Domain B.
632
+ * Same retry behavior as _signedGetEip712.
622
633
  */
623
634
  async _signedDeleteEip712(path, params, resolved) {
624
- const qs = await this._buildSignedQueryString(params, resolved);
625
- const url = `${this._baseUrl}${path}?${qs}`;
626
- const res = await fetch(url, { method: "DELETE" });
627
- return this._handleAsterResponse(res, "DELETE", path);
635
+ return this._signedRequestWithRetry("DELETE", path, params, resolved);
636
+ }
637
+ /** Shared GET/DELETE retry loop with fresh-nonce per attempt. */
638
+ async _signedRequestWithRetry(method, path, params, resolved) {
639
+ const MAX_ATTEMPTS = 3;
640
+ let lastErr;
641
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
642
+ // Rebuild signed query string with fresh nonce on each attempt.
643
+ const qs = await this._buildSignedQueryString(params, resolved);
644
+ const url = `${this._baseUrl}${path}?${qs}`;
645
+ const res = await fetch(url, method === "DELETE" ? { method: "DELETE" } : undefined);
646
+ // 429 → backoff + retry. All other paths (including non-429 venue
647
+ // errors wrapped in HTTP 200 with code≠0) throw immediately via
648
+ // _handleAsterResponse — no silent fallback (Rule #2).
649
+ if (res.status === 429 && attempt < MAX_ATTEMPTS - 1) {
650
+ const backoffMs = 2000 * Math.pow(2, attempt); // 2s, 4s, 8s
651
+ await new Promise((r) => setTimeout(r, backoffMs));
652
+ continue;
653
+ }
654
+ try {
655
+ return await this._handleAsterResponse(res, method, path);
656
+ }
657
+ catch (e) {
658
+ lastErr = e;
659
+ if (res.status === 429 && attempt < MAX_ATTEMPTS - 1) {
660
+ const backoffMs = 2000 * Math.pow(2, attempt);
661
+ await new Promise((r) => setTimeout(r, backoffMs));
662
+ continue;
663
+ }
664
+ throw e;
665
+ }
666
+ }
667
+ throw lastErr;
628
668
  }
629
669
  // ── Private: HTTP response handling ──────────────────────────────────────
630
670
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.12.11",
3
+ "version": "0.12.12",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -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.11"
8
+ version: "0.12.12"
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.11)
44
+ perp --version 2>/dev/null # check if exists (must be >= 0.12.12)
45
45
  npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
46
46
  ```
47
47