perp-cli 0.9.8 → 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.
Files changed (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1,36 +1,92 @@
1
1
  /**
2
- * Aster DEX adapter — Binance Futures API compatible REST wrapper.
2
+ * Aster DEX adapter — EIP-712 three-tier signer routing (v3.2).
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)
8
+ *
9
+ * HMAC paths fully removed (see Step 3 plan — all Binance-compat HMAC code deleted).
10
+ *
3
11
  * Docs: https://docs.asterdex.com/product/aster-perpetuals/api/api-documentation
4
12
  * Base: https://fapi.asterdex.com
5
13
  */
6
- import { createHmac } from "node:crypto";
14
+ import { isExpired } from "../agent-wallet/expiry.js";
15
+ import { classifyError, PerpError } from "../errors.js";
16
+ import { buildOrderTypedData } from "./aster-typed-data.js";
17
+ // ── AsterAdapter ──────────────────────────────────────────────────────────────
7
18
  export class AsterAdapter {
8
19
  name = "aster";
9
20
  chain = "bnb";
10
21
  aliases = ["ast"];
11
- _apiKey;
12
- _apiSecret;
13
22
  _baseUrl;
14
23
  _testnet;
24
+ // Signer tiers
25
+ _agentMeta;
26
+ _agentSigner; // Tier 1
27
+ _masterSigner; // Tier 2
28
+ _pkSigner; // Tier 3
29
+ _useNoAgent = false;
30
+ /** PK held between ctor and init() since LocalEvmSigner.create() is async */
31
+ _pendingPk;
32
+ // Cache
15
33
  _marketsCache = null;
16
34
  _marketsCacheTime = 0;
17
35
  _fundingHoursCache = new Map();
18
36
  _accountCache = null;
19
37
  _positionsCache = null;
20
38
  _ordersCache = null;
21
- static CACHE_TTL = 30_000; // 30 seconds (markets)
22
- static ACCOUNT_CACHE_TTL = 5_000; // 5 seconds (account/positions/orders)
23
- constructor(apiKey, apiSecret, testnet = false) {
24
- this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
25
- this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
39
+ static CACHE_TTL = 30_000;
40
+ static ACCOUNT_CACHE_TTL = 5_000;
41
+ // Nonce counter — ensures microsecond-precision uniqueness within process
42
+ _nonceCounter = 0;
43
+ constructor(privateKey, testnet = false) {
26
44
  this._testnet = testnet;
27
45
  this._baseUrl = testnet
28
46
  ? (process.env.ASTER_TESTNET_URL || "https://testnet.asterdex.com")
29
47
  : "https://fapi.asterdex.com";
48
+ if (privateKey) {
49
+ this._pendingPk = privateKey;
50
+ }
51
+ }
52
+ // ── Lifecycle ──
53
+ async init() {
54
+ // Verify connectivity by fetching server time
55
+ await this._publicGet("/fapi/v1/time");
56
+ // Build LocalEvmSigner from ctor PK now that we're in an async context
57
+ if (this._pendingPk) {
58
+ const { LocalEvmSigner } = await import("../signer/index.js");
59
+ this._pkSigner = await LocalEvmSigner.create(this._pendingPk);
60
+ this._pendingPk = undefined;
61
+ }
30
62
  }
63
+ // ── Signer injection ──
64
+ setAgent(meta, strategy) {
65
+ this._agentMeta = meta;
66
+ this._agentSigner = strategy;
67
+ }
68
+ setMasterSigner(signer) {
69
+ this._masterSigner = signer;
70
+ }
71
+ setPkSigner(signer) {
72
+ this._pkSigner = signer;
73
+ }
74
+ setNoAgent(noAgent) {
75
+ this._useNoAgent = noAgent;
76
+ }
77
+ // ── Status accessors ──
31
78
  get isReadOnly() {
32
- return !this._apiKey || !this._apiSecret;
79
+ return !this._agentSigner && !this._masterSigner && !this._pkSigner;
80
+ }
81
+ get activeSignerTier() {
82
+ try {
83
+ return this._resolveSigner().tier;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
33
88
  }
89
+ // ── Symbol helpers ──
34
90
  /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
35
91
  _toApi(symbol) {
36
92
  const s = symbol.toUpperCase().replace(/-PERP$/, "");
@@ -42,12 +98,8 @@ export class AsterAdapter {
42
98
  _fromApi(symbol) {
43
99
  return symbol.replace(/USDT$/, "").replace(/BUSD$/, "");
44
100
  }
45
- async init() {
46
- // Verify connectivity by fetching server time
47
- await this._publicGet("/fapi/v1/time");
48
- }
49
101
  // ── Market Data ──
50
- /** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
102
+ /** Get funding interval for a symbol (lazy bootstrap). */
51
103
  async getFundingHours(symbol) {
52
104
  const key = symbol.toUpperCase();
53
105
  const cached = this._fundingHoursCache.get(key);
@@ -64,11 +116,9 @@ export class AsterAdapter {
64
116
  }
65
117
  }
66
118
  catch { /* non-critical */ }
67
- // Don't cache a default — let strategy skip this symbol until real data is available
68
119
  return undefined;
69
120
  }
70
121
  async getMarkets() {
71
- // Return cached result if fresh
72
122
  if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
73
123
  return this._marketsCache;
74
124
  }
@@ -80,7 +130,6 @@ export class AsterAdapter {
80
130
  for (const t of tickers ?? []) {
81
131
  tickerMap.set(String(t.symbol), t);
82
132
  }
83
- // Also fetch premium index for mark/index/funding
84
133
  let premiumMap = new Map();
85
134
  try {
86
135
  const premiums = await this._publicGet("/fapi/v1/premiumIndex");
@@ -89,17 +138,13 @@ export class AsterAdapter {
89
138
  catch { /* non-critical */ }
90
139
  const tradingSymbols = (info?.symbols ?? [])
91
140
  .filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING");
92
- // OI is fetched lazily per symbol (see getMarketInfo) to avoid 300+ API calls
93
- const result = tradingSymbols
94
- .map((s) => {
141
+ const result = tradingSymbols.map((s) => {
95
142
  const sym = String(s.symbol);
96
143
  const ticker = tickerMap.get(sym);
97
144
  const premium = premiumMap.get(sym);
98
145
  const maxLev = Number(s.maxLeverage ?? 50);
99
146
  const lotFilter = s.filters
100
147
  ?.find(f => f.filterType === "LOT_SIZE");
101
- // Use cached funding interval if available (populated by getFundingHours()).
102
- // If not yet bootstrapped, fundingHours = undefined → strategy must skip this symbol.
103
148
  const fundingHours = this._fundingHoursCache.get(this._fromApi(sym));
104
149
  return {
105
150
  symbol: this._fromApi(sym),
@@ -173,7 +218,9 @@ export class AsterAdapter {
173
218
  if (this._accountCache && Date.now() - this._accountCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
174
219
  return this._accountCache.data;
175
220
  }
176
- const account = await this._signedGet("/fapi/v2/account");
221
+ const r = this._resolveSigner();
222
+ // v3: /fapi/v2/account is HMAC-only; v3 EIP-712 uses /fapi/v3/accountWithJoinMargin.
223
+ const account = await this._signedGetEip712("/fapi/v3/accountWithJoinMargin", {}, r);
177
224
  const totalWallet = Number(account.totalWalletBalance ?? 0);
178
225
  const unrealizedPnl = Number(account.totalUnrealizedProfit ?? 0);
179
226
  const available = Number(account.availableBalance ?? 0);
@@ -191,19 +238,35 @@ export class AsterAdapter {
191
238
  if (this._positionsCache && Date.now() - this._positionsCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
192
239
  return this._positionsCache.data;
193
240
  }
194
- const data = await this._signedGet("/fapi/v2/positionRisk");
195
- const result = (data ?? [])
196
- .filter((p) => Number(p.positionAmt ?? 0) !== 0)
197
- .map((p) => {
241
+ const r = this._resolveSigner();
242
+ // v3: /fapi/v2/positionRisk has no v3 equivalent. Positions live inside
243
+ // /fapi/v3/accountWithJoinMargin under `positions`. Mark price + liquidation
244
+ // price are NOT included there — fetch from the public premiumIndex.
245
+ const account = await this._signedGetEip712("/fapi/v3/accountWithJoinMargin", {}, r);
246
+ const positions = account.positions ?? [];
247
+ const open = positions.filter((p) => Number(p.positionAmt ?? 0) !== 0);
248
+ // Fetch mark prices for the open symbols (best-effort; non-fatal on failure)
249
+ const markMap = new Map();
250
+ if (open.length > 0) {
251
+ try {
252
+ const premiums = await this._publicGet("/fapi/v1/premiumIndex");
253
+ for (const p of premiums ?? []) {
254
+ markMap.set(String(p.symbol), String(p.markPrice ?? "0"));
255
+ }
256
+ }
257
+ catch { /* non-critical */ }
258
+ }
259
+ const result = open.map((p) => {
198
260
  const amt = Number(p.positionAmt ?? 0);
261
+ const apiSym = String(p.symbol ?? "");
199
262
  return {
200
- symbol: this._fromApi(String(p.symbol ?? "")),
263
+ symbol: this._fromApi(apiSym),
201
264
  side: amt > 0 ? "long" : "short",
202
265
  size: String(Math.abs(amt)),
203
266
  entryPrice: String(p.entryPrice ?? "0"),
204
- markPrice: String(p.markPrice ?? "0"),
205
- liquidationPrice: String(p.liquidationPrice ?? "0"),
206
- unrealizedPnl: String(p.unRealizedProfit ?? "0"),
267
+ markPrice: markMap.get(apiSym) ?? "0",
268
+ liquidationPrice: "0", // v3 accountWithJoinMargin does not expose this
269
+ unrealizedPnl: String(p.unrealizedProfit ?? "0"),
207
270
  leverage: Number(p.leverage ?? 1),
208
271
  };
209
272
  });
@@ -214,7 +277,8 @@ export class AsterAdapter {
214
277
  if (this._ordersCache && Date.now() - this._ordersCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
215
278
  return this._ordersCache.data;
216
279
  }
217
- const orders = await this._signedGet("/fapi/v1/openOrders");
280
+ const r = this._resolveSigner();
281
+ const orders = await this._signedGetEip712("/fapi/v3/openOrders", {}, r);
218
282
  const result = (orders ?? []).map((o) => ({
219
283
  orderId: String(o.orderId ?? ""),
220
284
  symbol: this._fromApi(String(o.symbol ?? "")),
@@ -229,22 +293,21 @@ export class AsterAdapter {
229
293
  return result;
230
294
  }
231
295
  async getOrderHistory(limit = 30) {
232
- // allOrders requires symbol — get from positions or recent trades
233
296
  const positions = await this.getPositions();
234
297
  const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
235
298
  if (apiSymbols.size === 0) {
236
- // Fallback: query the most common perp symbols when no open positions exist
237
299
  for (const s of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]) {
238
300
  apiSymbols.add(s);
239
301
  }
240
302
  }
303
+ const r = this._resolveSigner();
241
304
  const allOrders = [];
242
305
  for (const sym of apiSymbols) {
243
306
  try {
244
- const orders = await this._signedGet("/fapi/v1/allOrders", {
307
+ const orders = await this._signedGetEip712("/fapi/v3/allOrders", {
245
308
  symbol: sym,
246
309
  limit: String(limit),
247
- });
310
+ }, r);
248
311
  for (const o of orders ?? []) {
249
312
  allOrders.push({
250
313
  orderId: String(o.orderId ?? ""),
@@ -267,13 +330,14 @@ export class AsterAdapter {
267
330
  const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
268
331
  if (apiSymbols.size === 0)
269
332
  apiSymbols.add("BTCUSDT");
333
+ const r = this._resolveSigner();
270
334
  const allTrades = [];
271
335
  for (const sym of apiSymbols) {
272
336
  try {
273
- const trades = await this._signedGet("/fapi/v1/userTrades", {
337
+ const trades = await this._signedGetEip712("/fapi/v3/userTrades", {
274
338
  symbol: sym,
275
339
  limit: String(limit),
276
- });
340
+ }, r);
277
341
  for (const t of trades ?? []) {
278
342
  allTrades.push({
279
343
  time: Number(t.time ?? 0),
@@ -290,10 +354,11 @@ export class AsterAdapter {
290
354
  return allTrades.sort((a, b) => b.time - a.time).slice(0, limit);
291
355
  }
292
356
  async getFundingPayments(limit = 200) {
293
- const data = await this._signedGet("/fapi/v1/income", {
357
+ const r = this._resolveSigner();
358
+ const data = await this._signedGetEip712("/fapi/v3/income", {
294
359
  incomeType: "FUNDING_FEE",
295
360
  limit: String(limit),
296
- });
361
+ }, r);
297
362
  return (data ?? []).map((f) => ({
298
363
  time: Number(f.time ?? 0),
299
364
  symbol: this._fromApi(String(f.symbol ?? "")),
@@ -302,6 +367,7 @@ export class AsterAdapter {
302
367
  }
303
368
  // ── Trading ──
304
369
  async marketOrder(symbol, side, size, opts) {
370
+ const r = this._resolveSigner();
305
371
  const apiSymbol = this._toApi(symbol);
306
372
  const params = {
307
373
  symbol: apiSymbol,
@@ -311,25 +377,23 @@ export class AsterAdapter {
311
377
  };
312
378
  if (opts?.reduceOnly)
313
379
  params.reduceOnly = "true";
314
- const result = await this._signedPost("/fapi/v1/order", params);
315
- const r = result;
316
- const executedQty = Number(r.executedQty ?? 0);
317
- const orderId = String(r.orderId ?? "");
318
- // If immediately filled, return
319
- if (executedQty > 0 && r.status === "FILLED")
380
+ const result = await this._signedPostEip712("/fapi/v3/order", params, r);
381
+ const ro = result;
382
+ const executedQty = Number(ro.executedQty ?? 0);
383
+ const orderId = String(ro.orderId ?? "");
384
+ if (executedQty > 0 && ro.status === "FILLED")
320
385
  return result;
321
- // status=NEW: order accepted but not yet filled — poll for up to 3 seconds
322
- if (r.status === "NEW" && orderId) {
386
+ if (ro.status === "NEW" && orderId) {
323
387
  for (let i = 0; i < 3; i++) {
324
- await new Promise(resolve => setTimeout(resolve, 1000));
388
+ await new Promise(res => setTimeout(res, 1000));
325
389
  try {
326
- const check = await this._signedGet("/fapi/v1/order", {
390
+ const check = await this._signedGetEip712("/fapi/v3/order", {
327
391
  symbol: apiSymbol,
328
392
  orderId,
329
- });
393
+ }, r);
330
394
  const filledQty = Number(check.executedQty ?? 0);
331
395
  if (filledQty > 0)
332
- return check; // filled
396
+ return check;
333
397
  if (check.status === "CANCELED" || check.status === "EXPIRED" || check.status === "REJECTED") {
334
398
  throw new Error(`Market ${side} ${symbol}: order ${check.status} (orderId: ${orderId})`);
335
399
  }
@@ -337,23 +401,21 @@ export class AsterAdapter {
337
401
  catch (e) {
338
402
  if (e instanceof Error && e.message.startsWith("Market "))
339
403
  throw e;
340
- // query failed, retry
341
404
  }
342
405
  }
343
- // Still not filled after 3s — cancel and throw
344
406
  try {
345
- await this._signedDelete("/fapi/v1/order", { symbol: apiSymbol, orderId });
407
+ await this._signedDeleteEip712("/fapi/v3/order", { symbol: apiSymbol, orderId }, r);
346
408
  }
347
409
  catch { /* best effort cancel */ }
348
410
  throw new Error(`Market ${side} ${symbol}: order not filled after 3s, cancelled (orderId: ${orderId})`);
349
411
  }
350
- // executedQty === 0 with non-NEW status
351
412
  if (executedQty === 0) {
352
- throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${orderId})`);
413
+ throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${ro.status}, orderId: ${orderId})`);
353
414
  }
354
415
  return result;
355
416
  }
356
417
  async limitOrder(symbol, side, price, size, opts) {
418
+ const r = this._resolveSigner();
357
419
  const params = {
358
420
  symbol: this._toApi(symbol),
359
421
  side: side.toUpperCase(),
@@ -364,7 +426,7 @@ export class AsterAdapter {
364
426
  };
365
427
  if (opts?.reduceOnly)
366
428
  params.reduceOnly = "true";
367
- return this._signedPost("/fapi/v1/order", params);
429
+ return this._signedPostEip712("/fapi/v3/order", params, r);
368
430
  }
369
431
  async editOrder(symbol, orderId, price, size) {
370
432
  // Aster has no atomic edit — cancel + replace
@@ -375,44 +437,44 @@ export class AsterAdapter {
375
437
  return this.limitOrder(symbol, side, price, size);
376
438
  }
377
439
  async cancelOrder(symbol, orderId) {
378
- return this._signedDelete("/fapi/v1/order", {
440
+ const r = this._resolveSigner();
441
+ return this._signedDeleteEip712("/fapi/v3/order", {
379
442
  symbol: this._toApi(symbol),
380
443
  orderId,
381
- });
444
+ }, r);
382
445
  }
383
446
  async cancelAllOrders(symbol) {
447
+ const r = this._resolveSigner();
384
448
  if (!symbol) {
385
- // Cancel all for all symbols with open orders
386
449
  const orders = await this.getOpenOrders();
387
450
  const apiSymbols = new Set(orders.map(o => this._toApi(o.symbol)));
388
451
  const results = [];
389
452
  for (const sym of apiSymbols) {
390
- results.push(await this._signedDelete("/fapi/v1/allOpenOrders", { symbol: sym }));
453
+ results.push(await this._signedDeleteEip712("/fapi/v3/allOpenOrders", { symbol: sym }, r));
391
454
  }
392
455
  return results;
393
456
  }
394
- return this._signedDelete("/fapi/v1/allOpenOrders", { symbol: this._toApi(symbol) });
457
+ return this._signedDeleteEip712("/fapi/v3/allOpenOrders", { symbol: this._toApi(symbol) }, r);
395
458
  }
396
459
  // ── Risk ──
397
460
  async setLeverage(symbol, leverage, marginMode) {
398
- // Set margin type first if specified
461
+ const r = this._resolveSigner();
399
462
  if (marginMode) {
400
463
  try {
401
- await this._signedPost("/fapi/v1/marginType", {
464
+ await this._signedPostEip712("/fapi/v3/marginType", {
402
465
  symbol: this._toApi(symbol),
403
466
  marginType: marginMode === "cross" ? "CROSSED" : "ISOLATED",
404
- });
405
- }
406
- catch {
407
- // May fail if already set — non-critical
467
+ }, r);
408
468
  }
469
+ catch { /* may fail if already set */ }
409
470
  }
410
- return this._signedPost("/fapi/v1/leverage", {
471
+ return this._signedPostEip712("/fapi/v3/leverage", {
411
472
  symbol: this._toApi(symbol),
412
473
  leverage: String(leverage),
413
- });
474
+ }, r);
414
475
  }
415
476
  async stopOrder(symbol, side, size, triggerPrice, opts) {
477
+ const r = this._resolveSigner();
416
478
  const params = {
417
479
  symbol: this._toApi(symbol),
418
480
  side: side.toUpperCase(),
@@ -426,26 +488,155 @@ export class AsterAdapter {
426
488
  }
427
489
  if (opts?.reduceOnly)
428
490
  params.reduceOnly = "true";
429
- return this._signedPost("/fapi/v1/order", params);
491
+ return this._signedPostEip712("/fapi/v3/order", params, r);
430
492
  }
431
- // ── Withdraw (unified interface) ──
493
+ // ── Withdraw (master-only: never delegates to agent or PK) ──
432
494
  async withdraw(amount, _destination, _opts) {
433
- // Aster withdrawal is via separate spot/wallet API, not futures API
495
+ if (!this._masterSigner) {
496
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Aster withdrawal requires OWS master wallet (--ows). Agent and PK signers cannot authorize withdrawals.", {
497
+ remediation: "Use: perp -e aster --ows <walletName> funds withdraw",
498
+ });
499
+ }
500
+ // Aster withdrawal via spot/wallet API (not futures API) — not yet implemented.
434
501
  throw new Error("Aster withdrawal requires the spot API (not available in futures mode). Use the Aster web UI to withdraw.");
435
502
  }
436
- // ── Internal HTTP helpers ──
437
- _sign(params) {
438
- params.timestamp = String(Date.now());
439
- params.recvWindow = "5000";
440
- const queryString = Object.entries(params)
441
- .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
442
- .join("&");
443
- const signature = createHmac("sha256", this._apiSecret)
444
- .update(queryString)
445
- .digest("hex");
446
- return `${queryString}&signature=${signature}`;
503
+ // ── Private: signer resolution ────────────────────────────────────────────
504
+ _resolveSigner() {
505
+ // Tier 1: agent (when registered, not expired, --no-agent NOT set)
506
+ if (!this._useNoAgent && this._agentSigner && this._agentMeta) {
507
+ if (isExpired(this._agentMeta)) {
508
+ // Skip Tier 1 only if Tier 2/3 available; otherwise throw AGENT_EXPIRED
509
+ if (!this._masterSigner && !this._pkSigner) {
510
+ throw new PerpError("AGENT_EXPIRED", "Agent wallet has expired", {
511
+ remediation: "perp wallet agent approve aster --rotate",
512
+ });
513
+ }
514
+ // fall through to Tier 2/3
515
+ }
516
+ else {
517
+ return {
518
+ tier: "agent",
519
+ signer: this._agentSigner,
520
+ signerAddress: this._agentMeta.agentEvmAddress,
521
+ userAddress: this._agentMeta.userEvmAddress,
522
+ };
523
+ }
524
+ }
525
+ // Tier 2: OWS master
526
+ if (this._masterSigner) {
527
+ const addr = this._masterSigner.getAddress();
528
+ return { tier: "master", signer: this._masterSigner, signerAddress: addr, userAddress: addr };
529
+ }
530
+ // Tier 3: PK direct
531
+ if (this._pkSigner) {
532
+ const addr = this._pkSigner.getAddress();
533
+ return { tier: "pk", signer: this._pkSigner, signerAddress: addr, userAddress: addr };
534
+ }
535
+ // No signer at any tier
536
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No signing path configured for Aster", {
537
+ remediation: "Run one of: (a) perp wallet agent approve aster --master <wallet>; (b) perp wallet generate && perp -e aster --ows <wallet>; (c) export ASTER_PRIVATE_KEY=0x...",
538
+ });
539
+ }
540
+ // ── Private: nonce ────────────────────────────────────────────────────────
541
+ _nextNonce() {
542
+ const ms = Date.now() * 1000 + (++this._nonceCounter % 1000);
543
+ return String(ms);
544
+ }
545
+ // ── Private: EIP-712 signed HTTP helpers ─────────────────────────────────
546
+ /**
547
+ * Build the canonical signed-request URL per Aster v3 spec.
548
+ *
549
+ * Reference: https://github.com/asterdex/api-docs (V3, EN, send_by_url).
550
+ * The Python reference adds keys to the dict in this exact order:
551
+ * nonce, user, signer
552
+ * then URL-encodes the dict to form the EIP-712 `msg`. After signing,
553
+ * `&signature=` is appended to the same query string. Authority verification
554
+ * reconstructs `msg` from the URL query params minus `signature`, so the
555
+ * signed dict and the URL query string must be byte-identical except for
556
+ * the appended signature.
557
+ *
558
+ * `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
559
+ * scheme; including it breaks signature verification on every call.
560
+ */
561
+ async _buildSignedQueryString(params, resolved) {
562
+ // Insertion order (matches Python reference: nonce, user, signer last).
563
+ const fullParams = {
564
+ ...params,
565
+ nonce: this._nextNonce(),
566
+ user: resolved.userAddress,
567
+ signer: resolved.signerAddress,
568
+ };
569
+ const typed = buildOrderTypedData(fullParams);
570
+ // Sign via the appropriate signer type
571
+ const sigRaw = await resolved.signer.signTypedData(typed.domain, typed.types, typed.message);
572
+ // Normalize: AgentSigningStrategy returns { signature, r, s, v }; EvmSigner returns string
573
+ const sigHex = typeof sigRaw === "string" ? sigRaw : sigRaw.signature;
574
+ // typed.message.msg is the URL-encoded dict in the same order; reuse it
575
+ // to guarantee the byte-identical query string is sent on the wire.
576
+ return `${typed.message.msg}&signature=${sigHex}`;
577
+ }
578
+ /**
579
+ * Sign and POST to a path using EIP-712 Domain B.
580
+ */
581
+ async _signedPostEip712(path, params, resolved) {
582
+ const qs = await this._buildSignedQueryString(params, resolved);
583
+ const url = `${this._baseUrl}${path}?${qs}`;
584
+ const res = await fetch(url, {
585
+ method: "POST",
586
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
587
+ body: "",
588
+ });
589
+ return this._handleEip712Response(res, "POST", path);
447
590
  }
448
- /** Handle 429 rate limit with retry */
591
+ /**
592
+ * Sign and GET from a path using EIP-712 Domain B.
593
+ */
594
+ async _signedGetEip712(path, params, resolved) {
595
+ const qs = await this._buildSignedQueryString(params, resolved);
596
+ const url = `${this._baseUrl}${path}?${qs}`;
597
+ for (let attempt = 0; attempt < 3; attempt++) {
598
+ const res = await fetch(url);
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`);
604
+ }
605
+ /**
606
+ * Sign and DELETE from a path using EIP-712 Domain B.
607
+ */
608
+ async _signedDeleteEip712(path, params, resolved) {
609
+ const qs = await this._buildSignedQueryString(params, resolved);
610
+ const url = `${this._baseUrl}${path}?${qs}`;
611
+ for (let attempt = 0; attempt < 3; attempt++) {
612
+ const res = await fetch(url, { method: "DELETE" });
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`);
618
+ }
619
+ // ── Private: HTTP response handling ──────────────────────────────────────
620
+ async _handleEip712Response(res, method, path) {
621
+ if (res.status === 429) {
622
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
623
+ await new Promise(r => setTimeout(r, Math.min(retryAfter * 1000, 30000)));
624
+ throw new Error(`${method} ${path} rate limited`);
625
+ }
626
+ if (!res.ok) {
627
+ const text = await res.text().catch(() => "");
628
+ const s = classifyError(new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 200)}`), "aster");
629
+ throw new PerpError(s.code, s.message, { exchange: s.exchange });
630
+ }
631
+ const json = await res.json();
632
+ if (json.code !== undefined && json.code !== "000000" && json.code !== 200) {
633
+ const rawMsg = typeof json.msg === "string" ? json.msg : JSON.stringify(json);
634
+ const s = classifyError(new Error(rawMsg), "aster");
635
+ throw new PerpError(s.code, s.message, { exchange: s.exchange });
636
+ }
637
+ return json;
638
+ }
639
+ /** Handle 429 rate limit with retry — used by GET/DELETE multi-attempt loops */
449
640
  async _handleResponse(res, method, path, attempt = 0) {
450
641
  if (res.status === 429) {
451
642
  if (attempt >= 2)
@@ -475,53 +666,4 @@ export class AsterAdapter {
475
666
  }
476
667
  throw new Error(`GET ${path} failed: max retries exceeded`);
477
668
  }
478
- async _signedGet(path, params = {}) {
479
- if (this.isReadOnly)
480
- throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
481
- for (let attempt = 0; attempt < 3; attempt++) {
482
- const signed = this._sign({ ...params });
483
- const url = `${this._baseUrl}${path}?${signed}`;
484
- const res = await fetch(url, {
485
- headers: { "X-MBX-APIKEY": this._apiKey },
486
- });
487
- const result = await this._handleResponse(res, "GET", path, attempt);
488
- if (result !== null)
489
- return result;
490
- }
491
- throw new Error(`GET ${path} failed: max retries exceeded`);
492
- }
493
- async _signedPost(path, params = {}) {
494
- if (this.isReadOnly)
495
- throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
496
- for (let attempt = 0; attempt < 3; attempt++) {
497
- const signed = this._sign({ ...params });
498
- const res = await fetch(`${this._baseUrl}${path}`, {
499
- method: "POST",
500
- headers: {
501
- "X-MBX-APIKEY": this._apiKey,
502
- "Content-Type": "application/x-www-form-urlencoded",
503
- },
504
- body: signed,
505
- });
506
- const result = await this._handleResponse(res, "POST", path, attempt);
507
- if (result !== null)
508
- return result;
509
- }
510
- throw new Error(`POST ${path} failed: max retries exceeded`);
511
- }
512
- async _signedDelete(path, params = {}) {
513
- if (this.isReadOnly)
514
- throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
515
- for (let attempt = 0; attempt < 3; attempt++) {
516
- const signed = this._sign({ ...params });
517
- const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
518
- method: "DELETE",
519
- headers: { "X-MBX-APIKEY": this._apiKey },
520
- });
521
- const result = await this._handleResponse(res, "DELETE", path, attempt);
522
- if (result !== null)
523
- return result;
524
- }
525
- throw new Error(`DELETE ${path} failed: max retries exceeded`);
526
- }
527
669
  }