perp-cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) 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/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
package/dist/errors.d.ts CHANGED
@@ -50,6 +50,62 @@ export declare const ERROR_CODES: {
50
50
  readonly status: 409;
51
51
  readonly retryable: false;
52
52
  };
53
+ readonly NO_SIGNER_AVAILABLE: {
54
+ readonly code: "NO_SIGNER_AVAILABLE";
55
+ readonly status: 401;
56
+ readonly retryable: false;
57
+ };
58
+ readonly AGENT_NOT_REGISTERED: {
59
+ readonly code: "AGENT_NOT_REGISTERED";
60
+ readonly status: 401;
61
+ readonly retryable: false;
62
+ };
63
+ readonly AGENT_EXPIRED: {
64
+ readonly code: "AGENT_EXPIRED";
65
+ readonly status: 401;
66
+ readonly retryable: false;
67
+ };
68
+ readonly POLICY_DENIED: {
69
+ readonly code: "POLICY_DENIED";
70
+ readonly status: 403;
71
+ readonly retryable: false;
72
+ };
73
+ readonly KEY_NOT_FOUND: {
74
+ readonly code: "KEY_NOT_FOUND";
75
+ readonly status: 404;
76
+ readonly retryable: false;
77
+ };
78
+ readonly WALLET_LOCKED: {
79
+ readonly code: "WALLET_LOCKED";
80
+ readonly status: 423;
81
+ readonly retryable: false;
82
+ };
83
+ readonly APPROVE_PARTIAL: {
84
+ readonly code: "APPROVE_PARTIAL";
85
+ readonly status: 500;
86
+ readonly retryable: false;
87
+ };
88
+ readonly APPROVE_FAILED: {
89
+ readonly code: "APPROVE_FAILED";
90
+ readonly status: 500;
91
+ readonly retryable: false;
92
+ };
93
+ readonly LOCK_HELD: {
94
+ readonly code: "LOCK_HELD";
95
+ readonly status: 423;
96
+ readonly retryable: true;
97
+ readonly retryAfterMs: 5000;
98
+ };
99
+ readonly PASSPHRASE_REQUIRED: {
100
+ readonly code: "PASSPHRASE_REQUIRED";
101
+ readonly status: 401;
102
+ readonly retryable: false;
103
+ };
104
+ readonly NOT_IMPLEMENTED: {
105
+ readonly code: "NOT_IMPLEMENTED";
106
+ readonly status: 501;
107
+ readonly retryable: false;
108
+ };
53
109
  readonly EXCHANGE_UNREACHABLE: {
54
110
  readonly code: "EXCHANGE_UNREACHABLE";
55
111
  readonly status: 503;
@@ -96,7 +152,15 @@ export interface StructuredError {
96
152
  retryAfterMs?: number;
97
153
  exchange?: string;
98
154
  details?: Record<string, unknown>;
155
+ /** Actionable hint for automated callers (AC-19) */
156
+ remediation?: string;
99
157
  }
158
+ /**
159
+ * Extract a human-readable message from any thrown value.
160
+ * Handles Error instances, plain objects with a `.message` field (string OR object),
161
+ * and circular-safe stringification fallback.
162
+ */
163
+ export declare function extractErrorMessage(err: unknown): string;
100
164
  /**
101
165
  * Classify an error from any exchange into a structured error code.
102
166
  * Pattern-matches on error messages to detect known error types.
package/dist/errors.js CHANGED
@@ -11,6 +11,18 @@ export const ERROR_CODES = {
11
11
  SIZE_TOO_LARGE: { code: "SIZE_TOO_LARGE", status: 400, retryable: false },
12
12
  RISK_VIOLATION: { code: "RISK_VIOLATION", status: 403, retryable: false },
13
13
  DUPLICATE_ORDER: { code: "DUPLICATE_ORDER", status: 409, retryable: false },
14
+ // Agent-wallet error codes (OWS / Phase 2a)
15
+ NO_SIGNER_AVAILABLE: { code: "NO_SIGNER_AVAILABLE", status: 401, retryable: false },
16
+ AGENT_NOT_REGISTERED: { code: "AGENT_NOT_REGISTERED", status: 401, retryable: false },
17
+ AGENT_EXPIRED: { code: "AGENT_EXPIRED", status: 401, retryable: false },
18
+ POLICY_DENIED: { code: "POLICY_DENIED", status: 403, retryable: false },
19
+ KEY_NOT_FOUND: { code: "KEY_NOT_FOUND", status: 404, retryable: false },
20
+ WALLET_LOCKED: { code: "WALLET_LOCKED", status: 423, retryable: false },
21
+ APPROVE_PARTIAL: { code: "APPROVE_PARTIAL", status: 500, retryable: false },
22
+ APPROVE_FAILED: { code: "APPROVE_FAILED", status: 500, retryable: false },
23
+ LOCK_HELD: { code: "LOCK_HELD", status: 423, retryable: true, retryAfterMs: 5000 },
24
+ PASSPHRASE_REQUIRED: { code: "PASSPHRASE_REQUIRED", status: 401, retryable: false },
25
+ NOT_IMPLEMENTED: { code: "NOT_IMPLEMENTED", status: 501, retryable: false },
14
26
  // 5xx - System / transient errors
15
27
  EXCHANGE_UNREACHABLE: { code: "EXCHANGE_UNREACHABLE", status: 503, retryable: true },
16
28
  RATE_LIMITED: { code: "RATE_LIMITED", status: 429, retryable: true, retryAfterMs: 1000 },
@@ -20,12 +32,37 @@ export const ERROR_CODES = {
20
32
  TIMEOUT: { code: "TIMEOUT", status: 504, retryable: true },
21
33
  UNKNOWN: { code: "UNKNOWN", status: 500, retryable: false },
22
34
  };
35
+ /**
36
+ * Extract a human-readable message from any thrown value.
37
+ * Handles Error instances, plain objects with a `.message` field (string OR object),
38
+ * and circular-safe stringification fallback.
39
+ */
40
+ export function extractErrorMessage(err) {
41
+ if (err instanceof Error)
42
+ return err.message;
43
+ if (err && typeof err === "object") {
44
+ const m = err.message;
45
+ if (typeof m === "string")
46
+ return m;
47
+ if (m !== undefined) {
48
+ try {
49
+ return JSON.stringify(m);
50
+ }
51
+ catch { /* circular */ }
52
+ }
53
+ try {
54
+ return JSON.stringify(err);
55
+ }
56
+ catch { /* circular */ }
57
+ }
58
+ return String(err);
59
+ }
23
60
  /**
24
61
  * Classify an error from any exchange into a structured error code.
25
62
  * Pattern-matches on error messages to detect known error types.
26
63
  */
27
64
  export function classifyError(err, exchange) {
28
- const message = err instanceof Error ? err.message : String(err);
65
+ const message = extractErrorMessage(err);
29
66
  const lower = message.toLowerCase();
30
67
  // Margin-specific checks first (before generic "insufficient" catch)
31
68
  if (lower.includes("margin") && (lower.includes("insufficient") || lower.includes("not enough"))) {
@@ -43,6 +80,39 @@ export function classifyError(err, exchange) {
43
80
  if (lower.includes("timeout") || lower.includes("timed out") || lower.includes("etimedout")) {
44
81
  return { ...ERROR_CODES.TIMEOUT, message, exchange };
45
82
  }
83
+ // OWS / agent-wallet specific matchers — MUST precede generic "sign" / "not found" blocks
84
+ // so "wallet is locked, cannot sign" → WALLET_LOCKED (not SIGNATURE_FAILED)
85
+ // and "key not found" → KEY_NOT_FOUND (not SYMBOL_NOT_FOUND / default)
86
+ if (lower.includes("policy denied") || (lower.includes("policy") && lower.includes("denied"))) {
87
+ return { ...ERROR_CODES.POLICY_DENIED, message, exchange };
88
+ }
89
+ if (lower.includes("wallet is locked") || lower.includes("wallet locked")) {
90
+ return { ...ERROR_CODES.WALLET_LOCKED, message, exchange };
91
+ }
92
+ if (lower.includes("key not found") || lower.includes("api key not found")) {
93
+ return { ...ERROR_CODES.KEY_NOT_FOUND, message, exchange };
94
+ }
95
+ if (lower.includes("no signer") || lower.includes("signer unavailable") || lower.includes("no auth")) {
96
+ return { ...ERROR_CODES.NO_SIGNER_AVAILABLE, message, exchange };
97
+ }
98
+ if (lower.includes("agent not registered")) {
99
+ return { ...ERROR_CODES.AGENT_NOT_REGISTERED, message, exchange };
100
+ }
101
+ if (lower.includes("agent expired") || lower.includes("agent has expired")) {
102
+ return { ...ERROR_CODES.AGENT_EXPIRED, message, exchange };
103
+ }
104
+ if (lower.includes("lock held") || lower.includes("lockfile held")) {
105
+ return { ...ERROR_CODES.LOCK_HELD, message, exchange };
106
+ }
107
+ if (lower.includes("passphrase required") || lower.includes("passphrase missing")) {
108
+ return { ...ERROR_CODES.PASSPHRASE_REQUIRED, message, exchange };
109
+ }
110
+ if (lower.includes("partial") && (lower.includes("approve") || lower.includes("approval"))) {
111
+ return { ...ERROR_CODES.APPROVE_PARTIAL, message, exchange };
112
+ }
113
+ if (lower.includes("approve_failed") || (lower.includes("approve failed") && lower.includes("clean"))) {
114
+ return { ...ERROR_CODES.APPROVE_FAILED, message, exchange };
115
+ }
46
116
  if (lower.includes("not found") && (lower.includes("symbol") || lower.includes("market") || lower.includes("asset"))) {
47
117
  return { ...ERROR_CODES.SYMBOL_NOT_FOUND, message, exchange };
48
118
  }
@@ -79,6 +149,16 @@ export class PerpError extends Error {
79
149
  constructor(code, message, details) {
80
150
  super(message);
81
151
  this.name = "PerpError";
82
- this.structured = { ...ERROR_CODES[code], message, details };
152
+ // Lift `remediation` from details to top-level StructuredError field (AC-19)
153
+ const remediation = details?.remediation;
154
+ const strippedDetails = details ? { ...details } : undefined;
155
+ if (strippedDetails)
156
+ delete strippedDetails.remediation;
157
+ this.structured = {
158
+ ...ERROR_CODES[code],
159
+ message,
160
+ ...(remediation !== undefined ? { remediation } : {}),
161
+ ...(strippedDetails && Object.keys(strippedDetails).length > 0 ? { details: strippedDetails } : {}),
162
+ };
83
163
  }
84
164
  }
@@ -48,9 +48,20 @@ export async function startEventStream(adapter, opts) {
48
48
  }
49
49
  }
50
50
  // Closed positions
51
+ // SSOT rule #2: emit realizedPnl explicitly so position-history can
52
+ // require the field. We can't reach back into the exchange to fetch the
53
+ // true realized P&L from a polling stream — but the last unrealized
54
+ // snapshot before the position disappears is the closest proxy and
55
+ // becomes realized at close. Surfacing it under realizedPnl makes the
56
+ // contract explicit.
51
57
  for (const [sym, prev] of prevPositions) {
52
58
  if (!currentPositions.has(sym)) {
53
- emit({ type: "position_closed", exchange: adapter.name, timestamp: ts, data: { ...prev } });
59
+ emit({
60
+ type: "position_closed",
61
+ exchange: adapter.name,
62
+ timestamp: ts,
63
+ data: { ...prev, realizedPnl: prev.unrealizedPnl },
64
+ });
54
65
  }
55
66
  }
56
67
  // ── Liquidation warnings ──
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Aster V3 EIP-712 typed-data builders.
3
+ *
4
+ * Pure functions — no side effects, no fs/fetch/env reads.
5
+ * All field ordering and serialization matches the HypurrQuant_FE mainnet
6
+ * production reference: AsterPerpAdapter.ts:456-700.
7
+ *
8
+ * Key conventions:
9
+ * - Domain A (chainId=56) — master-signed ops: approveAgent, delAgent, approveBuilder
10
+ * - Domain B (chainId=1666) — agent-signed per-order ops
11
+ * - uint256 fields serialized as decimal strings (never BigInt) to survive JSON.stringify
12
+ * - EIP-712 message keys are PascalCase; HTTP query-string keys are camelCase
13
+ */
14
+ /** Domain A: used for master-signed ops (ApproveAgent, DelAgent, ApproveBuilder). */
15
+ export declare const ASTER_DOMAIN_A: {
16
+ readonly name: "AsterSignTransaction";
17
+ readonly version: "1";
18
+ readonly chainId: 56;
19
+ readonly verifyingContract: "0x0000000000000000000000000000000000000000";
20
+ };
21
+ /** Domain B: used for agent-signed per-request ops (order, cancel, leverage). */
22
+ export declare const ASTER_DOMAIN_B: {
23
+ readonly chainId: 1666;
24
+ readonly name: "AsterSignTransaction";
25
+ readonly version: "1";
26
+ readonly verifyingContract: "0x0000000000000000000000000000000000000000";
27
+ };
28
+ /** EIP-712 types for Domain B order signing — single msg string (URL-encoded query string). */
29
+ export declare const ORDER_TYPES: {
30
+ readonly Message: readonly [{
31
+ readonly name: "msg";
32
+ readonly type: "string";
33
+ }];
34
+ };
35
+ type Eip712Field = {
36
+ name: string;
37
+ type: string;
38
+ };
39
+ type AsterChain = "Mainnet" | "Testnet";
40
+ export interface ApproveAgentParams {
41
+ user: `0x${string}`;
42
+ agentAddress: `0x${string}`;
43
+ agentName: string;
44
+ /** Unix milliseconds timestamp when the agent expires. Serialized as decimal string. */
45
+ expiredMs: number | string;
46
+ canPerpTrade: boolean;
47
+ canSpotTrade: boolean;
48
+ canWithdraw: boolean;
49
+ /**
50
+ * Optional IP whitelist. Include (even as "") when defined and non-null.
51
+ * Undefined/null = field omitted from EIP-712 struct.
52
+ */
53
+ ipWhitelist?: string;
54
+ /** Optional builder address. When present, Builder + MaxFeeRate are included. */
55
+ builder?: string;
56
+ /** Required when builder is set. */
57
+ maxFeeRate?: string;
58
+ /** Optional builder name. Independently gated by its own undefined-check. */
59
+ builderName?: string;
60
+ /** Microsecond-precision nonce (Date.now() * 1000 + counter). Serialized as decimal string. */
61
+ nonceMicros: number | string;
62
+ asterChain?: AsterChain;
63
+ }
64
+ /**
65
+ * Build the EIP-712 typed-data payload for `POST /fapi/v3/approveAgent`.
66
+ *
67
+ * Field order in types array and message object matches HypurrQuant_FE
68
+ * AsterPerpAdapter.ts:481-533 exactly:
69
+ * AgentName, AgentAddress, [IpWhitelist?,] Expired, CanSpotTrade, CanPerpTrade,
70
+ * CanWithdraw, [Builder?, MaxFeeRate?, BuilderName?,] AsterChain, User, Nonce
71
+ */
72
+ export declare function buildApproveAgentTypedData(params: ApproveAgentParams): {
73
+ domain: typeof ASTER_DOMAIN_A;
74
+ types: {
75
+ ApproveAgent: Eip712Field[];
76
+ };
77
+ primaryType: "ApproveAgent";
78
+ message: Record<string, unknown>;
79
+ };
80
+ export interface DelAgentParams {
81
+ user: `0x${string}`;
82
+ agentAddress: `0x${string}`;
83
+ nonceMicros: number | string;
84
+ asterChain?: AsterChain;
85
+ }
86
+ /**
87
+ * Build the EIP-712 typed-data payload for `DELETE /fapi/v3/agent`.
88
+ *
89
+ * TODO: confirm DelAgent struct vs Aster docs at Step 0b spike.
90
+ * HypurrQuant_FE does not implement a revoke/delAgent method — this is a
91
+ * best-effort field set mirroring the ApproveAgent terminal fields (AgentAddress,
92
+ * AsterChain, User, Nonce) without the permission booleans.
93
+ */
94
+ export declare function buildDelAgentTypedData(params: DelAgentParams): {
95
+ domain: typeof ASTER_DOMAIN_A;
96
+ types: {
97
+ DelAgent: Eip712Field[];
98
+ };
99
+ primaryType: "DelAgent";
100
+ message: Record<string, unknown>;
101
+ };
102
+ /**
103
+ * Encode order parameters as a URL-encoded query string.
104
+ *
105
+ * Deterministic key order = insertion order of `orderParams` (JS object property
106
+ * iteration order). Caller must pre-populate `user` and `nonce` (microseconds)
107
+ * in the dict; this function does NOT inject defaults.
108
+ *
109
+ * Uses `URLSearchParams` for encoding consistency with Python
110
+ * `urllib.parse.urlencode`.
111
+ */
112
+ export declare function buildOrderQueryStringMsg(orderParams: Record<string, string | number | boolean>): string;
113
+ /**
114
+ * Build the EIP-712 typed-data payload for agent-signed order requests.
115
+ *
116
+ * Domain B (chainId=1666), primaryType="Message", single `msg` field containing
117
+ * the URL-encoded query string produced by buildOrderQueryStringMsg.
118
+ */
119
+ export declare function buildOrderTypedData(orderParams: Record<string, string | number | boolean>): {
120
+ domain: typeof ASTER_DOMAIN_B;
121
+ types: typeof ORDER_TYPES;
122
+ primaryType: "Message";
123
+ message: {
124
+ msg: string;
125
+ };
126
+ };
127
+ export interface ApproveBuilderParams {
128
+ builder: string;
129
+ maxFeeRate: string;
130
+ builderName?: string;
131
+ user: `0x${string}`;
132
+ nonceMicros: number | string;
133
+ asterChain?: AsterChain;
134
+ }
135
+ /**
136
+ * Build the EIP-712 typed-data payload for `POST /fapi/v3/approveBuilder`.
137
+ *
138
+ * Domain A (chainId=56), primaryType="ApproveBuilder".
139
+ * Default-path field order from HypurrQuant_FE AsterPerpAdapter.ts:630-651:
140
+ * Builder, MaxFeeRate, AsterChain, User, Nonce
141
+ * `BuilderName` is a forward-compatible extension (HQ_FE omits it); included
142
+ * only when explicitly provided so the default path stays byte-equivalent to
143
+ * the production reference.
144
+ *
145
+ * Must be called BEFORE approveAgent so Aster fee attribution is set up
146
+ * before the first order.
147
+ */
148
+ export declare function buildApproveBuilderTypedData(params: ApproveBuilderParams): {
149
+ domain: typeof ASTER_DOMAIN_A;
150
+ types: {
151
+ ApproveBuilder: Eip712Field[];
152
+ };
153
+ primaryType: "ApproveBuilder";
154
+ message: Record<string, unknown>;
155
+ };
156
+ export {};
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Aster V3 EIP-712 typed-data builders.
3
+ *
4
+ * Pure functions — no side effects, no fs/fetch/env reads.
5
+ * All field ordering and serialization matches the HypurrQuant_FE mainnet
6
+ * production reference: AsterPerpAdapter.ts:456-700.
7
+ *
8
+ * Key conventions:
9
+ * - Domain A (chainId=56) — master-signed ops: approveAgent, delAgent, approveBuilder
10
+ * - Domain B (chainId=1666) — agent-signed per-order ops
11
+ * - uint256 fields serialized as decimal strings (never BigInt) to survive JSON.stringify
12
+ * - EIP-712 message keys are PascalCase; HTTP query-string keys are camelCase
13
+ */
14
+ // ── Domain constants ──────────────────────────────────────────────────────────
15
+ /** Domain A: used for master-signed ops (ApproveAgent, DelAgent, ApproveBuilder). */
16
+ export const ASTER_DOMAIN_A = {
17
+ name: "AsterSignTransaction",
18
+ version: "1",
19
+ chainId: 56,
20
+ verifyingContract: "0x0000000000000000000000000000000000000000",
21
+ };
22
+ /** Domain B: used for agent-signed per-request ops (order, cancel, leverage). */
23
+ export const ASTER_DOMAIN_B = {
24
+ ...ASTER_DOMAIN_A,
25
+ chainId: 1666,
26
+ };
27
+ /** EIP-712 types for Domain B order signing — single msg string (URL-encoded query string). */
28
+ export const ORDER_TYPES = {
29
+ Message: [{ name: "msg", type: "string" }],
30
+ };
31
+ /**
32
+ * Build the EIP-712 typed-data payload for `POST /fapi/v3/approveAgent`.
33
+ *
34
+ * Field order in types array and message object matches HypurrQuant_FE
35
+ * AsterPerpAdapter.ts:481-533 exactly:
36
+ * AgentName, AgentAddress, [IpWhitelist?,] Expired, CanSpotTrade, CanPerpTrade,
37
+ * CanWithdraw, [Builder?, MaxFeeRate?, BuilderName?,] AsterChain, User, Nonce
38
+ */
39
+ export function buildApproveAgentTypedData(params) {
40
+ const ipWhitelistIncluded = params.ipWhitelist !== undefined && params.ipWhitelist !== null;
41
+ const builderIncluded = params.builder !== undefined && params.builder !== null;
42
+ // Build types array — insertion order must match message object order.
43
+ const agentTypes = [
44
+ { name: "AgentName", type: "string" },
45
+ { name: "AgentAddress", type: "string" },
46
+ ];
47
+ if (ipWhitelistIncluded) {
48
+ agentTypes.push({ name: "IpWhitelist", type: "string" });
49
+ }
50
+ agentTypes.push({ name: "Expired", type: "uint256" }, { name: "CanSpotTrade", type: "bool" }, { name: "CanPerpTrade", type: "bool" }, { name: "CanWithdraw", type: "bool" });
51
+ if (builderIncluded) {
52
+ agentTypes.push({ name: "Builder", type: "string" });
53
+ agentTypes.push({ name: "MaxFeeRate", type: "string" });
54
+ if (params.builderName !== undefined) {
55
+ agentTypes.push({ name: "BuilderName", type: "string" });
56
+ }
57
+ }
58
+ agentTypes.push({ name: "AsterChain", type: "string" }, { name: "User", type: "string" }, { name: "Nonce", type: "uint256" });
59
+ // Build message object — same insertion order as types array.
60
+ const message = {
61
+ AgentName: params.agentName,
62
+ AgentAddress: params.agentAddress,
63
+ };
64
+ if (ipWhitelistIncluded) {
65
+ message["IpWhitelist"] = params.ipWhitelist;
66
+ }
67
+ // uint256 → decimal string (BigInt throws in JSON.stringify inside providers)
68
+ message["Expired"] = String(params.expiredMs);
69
+ message["CanSpotTrade"] = params.canSpotTrade;
70
+ message["CanPerpTrade"] = params.canPerpTrade;
71
+ message["CanWithdraw"] = params.canWithdraw;
72
+ if (builderIncluded) {
73
+ message["Builder"] = params.builder;
74
+ message["MaxFeeRate"] = params.maxFeeRate;
75
+ if (params.builderName !== undefined) {
76
+ message["BuilderName"] = params.builderName;
77
+ }
78
+ }
79
+ message["AsterChain"] = params.asterChain ?? "Mainnet";
80
+ message["User"] = params.user;
81
+ message["Nonce"] = String(params.nonceMicros);
82
+ return {
83
+ domain: ASTER_DOMAIN_A,
84
+ types: { ApproveAgent: agentTypes },
85
+ primaryType: "ApproveAgent",
86
+ message,
87
+ };
88
+ }
89
+ /**
90
+ * Build the EIP-712 typed-data payload for `DELETE /fapi/v3/agent`.
91
+ *
92
+ * TODO: confirm DelAgent struct vs Aster docs at Step 0b spike.
93
+ * HypurrQuant_FE does not implement a revoke/delAgent method — this is a
94
+ * best-effort field set mirroring the ApproveAgent terminal fields (AgentAddress,
95
+ * AsterChain, User, Nonce) without the permission booleans.
96
+ */
97
+ export function buildDelAgentTypedData(params) {
98
+ const delTypes = [
99
+ { name: "AgentAddress", type: "string" },
100
+ { name: "AsterChain", type: "string" },
101
+ { name: "User", type: "string" },
102
+ { name: "Nonce", type: "uint256" },
103
+ ];
104
+ const message = {
105
+ AgentAddress: params.agentAddress,
106
+ AsterChain: params.asterChain ?? "Mainnet",
107
+ User: params.user,
108
+ Nonce: String(params.nonceMicros),
109
+ };
110
+ return {
111
+ domain: ASTER_DOMAIN_A,
112
+ types: { DelAgent: delTypes },
113
+ primaryType: "DelAgent",
114
+ message,
115
+ };
116
+ }
117
+ // ── buildOrderQueryStringMsg ──────────────────────────────────────────────────
118
+ /**
119
+ * Encode order parameters as a URL-encoded query string.
120
+ *
121
+ * Deterministic key order = insertion order of `orderParams` (JS object property
122
+ * iteration order). Caller must pre-populate `user` and `nonce` (microseconds)
123
+ * in the dict; this function does NOT inject defaults.
124
+ *
125
+ * Uses `URLSearchParams` for encoding consistency with Python
126
+ * `urllib.parse.urlencode`.
127
+ */
128
+ export function buildOrderQueryStringMsg(orderParams) {
129
+ const sp = new URLSearchParams();
130
+ for (const [key, value] of Object.entries(orderParams)) {
131
+ sp.append(key, String(value));
132
+ }
133
+ return sp.toString();
134
+ }
135
+ // ── buildOrderTypedData ───────────────────────────────────────────────────────
136
+ /**
137
+ * Build the EIP-712 typed-data payload for agent-signed order requests.
138
+ *
139
+ * Domain B (chainId=1666), primaryType="Message", single `msg` field containing
140
+ * the URL-encoded query string produced by buildOrderQueryStringMsg.
141
+ */
142
+ export function buildOrderTypedData(orderParams) {
143
+ const msg = buildOrderQueryStringMsg(orderParams);
144
+ return {
145
+ domain: ASTER_DOMAIN_B,
146
+ types: ORDER_TYPES,
147
+ primaryType: "Message",
148
+ message: { msg },
149
+ };
150
+ }
151
+ /**
152
+ * Build the EIP-712 typed-data payload for `POST /fapi/v3/approveBuilder`.
153
+ *
154
+ * Domain A (chainId=56), primaryType="ApproveBuilder".
155
+ * Default-path field order from HypurrQuant_FE AsterPerpAdapter.ts:630-651:
156
+ * Builder, MaxFeeRate, AsterChain, User, Nonce
157
+ * `BuilderName` is a forward-compatible extension (HQ_FE omits it); included
158
+ * only when explicitly provided so the default path stays byte-equivalent to
159
+ * the production reference.
160
+ *
161
+ * Must be called BEFORE approveAgent so Aster fee attribution is set up
162
+ * before the first order.
163
+ */
164
+ export function buildApproveBuilderTypedData(params) {
165
+ const builderTypes = [
166
+ { name: "Builder", type: "string" },
167
+ { name: "MaxFeeRate", type: "string" },
168
+ ];
169
+ if (params.builderName !== undefined) {
170
+ builderTypes.push({ name: "BuilderName", type: "string" });
171
+ }
172
+ builderTypes.push({ name: "AsterChain", type: "string" }, { name: "User", type: "string" }, { name: "Nonce", type: "uint256" });
173
+ const message = {
174
+ Builder: params.builder,
175
+ MaxFeeRate: params.maxFeeRate,
176
+ };
177
+ if (params.builderName !== undefined) {
178
+ message["BuilderName"] = params.builderName;
179
+ }
180
+ message["AsterChain"] = params.asterChain ?? "Mainnet";
181
+ message["User"] = params.user;
182
+ message["Nonce"] = String(params.nonceMicros);
183
+ return {
184
+ domain: ASTER_DOMAIN_A,
185
+ types: { ApproveBuilder: builderTypes },
186
+ primaryType: "ApproveBuilder",
187
+ message,
188
+ };
189
+ }
@@ -1,17 +1,33 @@
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
14
  import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
15
+ import type { EvmSigner } from "../signer/interface.js";
16
+ import type { AgentMeta } from "../settings.js";
17
+ import type { AgentSigningStrategy } from "../agent-wallet/signing-strategy.js";
7
18
  export declare class AsterAdapter implements ExchangeAdapter {
8
19
  readonly name = "aster";
9
20
  readonly chain = "bnb";
10
21
  readonly aliases: readonly ["ast"];
11
- private _apiKey;
12
- private _apiSecret;
13
22
  private _baseUrl;
14
23
  private _testnet;
24
+ private _agentMeta?;
25
+ private _agentSigner?;
26
+ private _masterSigner?;
27
+ private _pkSigner?;
28
+ private _useNoAgent;
29
+ /** PK held between ctor and init() since LocalEvmSigner.create() is async */
30
+ private _pendingPk?;
15
31
  private _marketsCache;
16
32
  private _marketsCacheTime;
17
33
  private _fundingHoursCache;
@@ -20,14 +36,20 @@ export declare class AsterAdapter implements ExchangeAdapter {
20
36
  private _ordersCache;
21
37
  private static readonly CACHE_TTL;
22
38
  private static readonly ACCOUNT_CACHE_TTL;
23
- constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
39
+ private _nonceCounter;
40
+ constructor(privateKey?: string, testnet?: boolean);
41
+ init(): Promise<void>;
42
+ setAgent(meta: AgentMeta, strategy: AgentSigningStrategy): void;
43
+ setMasterSigner(signer: EvmSigner): void;
44
+ setPkSigner(signer: EvmSigner): void;
45
+ setNoAgent(noAgent: boolean): void;
24
46
  get isReadOnly(): boolean;
47
+ get activeSignerTier(): "agent" | "master" | "pk" | null;
25
48
  /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
26
49
  private _toApi;
27
50
  /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
28
51
  private _fromApi;
29
- init(): Promise<void>;
30
- /** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
52
+ /** Get funding interval for a symbol (lazy bootstrap). */
31
53
  getFundingHours(symbol: string): Promise<number | undefined>;
32
54
  getMarkets(): Promise<ExchangeMarketInfo[]>;
33
55
  getOrderbook(symbol: string): Promise<{
@@ -66,11 +88,38 @@ export declare class AsterAdapter implements ExchangeAdapter {
66
88
  assetId?: number;
67
89
  routeType?: number;
68
90
  }): Promise<unknown>;
69
- private _sign;
70
- /** Handle 429 rate limit with retry */
91
+ private _resolveSigner;
92
+ private _nextNonce;
93
+ /**
94
+ * Build the canonical signed-request URL per Aster v3 spec.
95
+ *
96
+ * Reference: https://github.com/asterdex/api-docs (V3, EN, send_by_url).
97
+ * The Python reference adds keys to the dict in this exact order:
98
+ * nonce, user, signer
99
+ * then URL-encodes the dict to form the EIP-712 `msg`. After signing,
100
+ * `&signature=` is appended to the same query string. Authority verification
101
+ * reconstructs `msg` from the URL query params minus `signature`, so the
102
+ * signed dict and the URL query string must be byte-identical except for
103
+ * the appended signature.
104
+ *
105
+ * `signatureChainId` is NOT a v3 parameter — it's an artifact of an older
106
+ * scheme; including it breaks signature verification on every call.
107
+ */
108
+ private _buildSignedQueryString;
109
+ /**
110
+ * Sign and POST to a path using EIP-712 Domain B.
111
+ */
112
+ private _signedPostEip712;
113
+ /**
114
+ * Sign and GET from a path using EIP-712 Domain B.
115
+ */
116
+ private _signedGetEip712;
117
+ /**
118
+ * Sign and DELETE from a path using EIP-712 Domain B.
119
+ */
120
+ private _signedDeleteEip712;
121
+ private _handleEip712Response;
122
+ /** Handle 429 rate limit with retry — used by GET/DELETE multi-attempt loops */
71
123
  private _handleResponse;
72
124
  private _publicGet;
73
- private _signedGet;
74
- private _signedPost;
75
- private _signedDelete;
76
125
  }