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
package/dist/config.d.ts CHANGED
@@ -5,6 +5,15 @@ export type Exchange = "pacifica" | "hyperliquid" | "lighter" | "aster";
5
5
  * Returns null if no key is configured.
6
6
  */
7
7
  export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string | null>;
8
+ /**
9
+ * Resolve an OWS wallet name to a signer-ready marker.
10
+ * Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
11
+ */
12
+ export declare function resolveOwsWallet(walletName: string): string | null;
13
+ /** Check if a key string represents an OWS wallet reference. */
14
+ export declare function isOwsKey(key: string): boolean;
15
+ /** Extract OWS wallet name from an ows: prefixed key string. */
16
+ export declare function getOwsWalletName(key: string): string;
8
17
  export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string>;
9
18
  export declare function parseSolanaKeypair(input: string): Keypair;
10
19
  export declare function isEvmPrivateKey(input: string): boolean;
package/dist/config.js CHANGED
@@ -3,6 +3,8 @@ import bs58 from "bs58";
3
3
  import chalk from "chalk";
4
4
  import { resolve } from "path";
5
5
  import { existsSync, readFileSync } from "fs";
6
+ import { createRequire } from "node:module";
7
+ const _require = createRequire(import.meta.url);
6
8
  /**
7
9
  * Try to load a private key without throwing if none is found.
8
10
  * Returns null if no key is configured.
@@ -15,9 +17,40 @@ export async function tryLoadPrivateKey(exchange, pkOverride, walletName) {
15
17
  return null;
16
18
  }
17
19
  }
20
+ /**
21
+ * Resolve an OWS wallet name to a signer-ready marker.
22
+ * Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
23
+ */
24
+ export function resolveOwsWallet(walletName) {
25
+ try {
26
+ const { getWallet } = _require("@open-wallet-standard/core");
27
+ getWallet(walletName); // throws if not found
28
+ return `ows:${walletName}`;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** Check if a key string represents an OWS wallet reference. */
35
+ export function isOwsKey(key) {
36
+ return key.startsWith("ows:");
37
+ }
38
+ /** Extract OWS wallet name from an ows: prefixed key string. */
39
+ export function getOwsWalletName(key) {
40
+ return key.slice(4);
41
+ }
18
42
  export async function loadPrivateKey(exchange, pkOverride, walletName) {
19
- // 0. Wallet name override (--wallet flag)
43
+ // 0a. OWS wallet (--ows flag sets walletName with "ows:" prefix)
44
+ if (walletName?.startsWith("ows:")) {
45
+ return walletName; // pass through — adapters detect the ows: prefix
46
+ }
47
+ // 0b. Wallet name override (--wallet flag) — check OWS first, then local
20
48
  if (walletName) {
49
+ // Try OWS vault
50
+ const owsKey = resolveOwsWallet(walletName);
51
+ if (owsKey)
52
+ return owsKey;
53
+ // Fall back to local wallets
21
54
  const { getWalletKeyByName } = await import("./commands/wallet.js");
22
55
  const key = getWalletKeyByName(walletName);
23
56
  if (!key)
@@ -27,7 +60,20 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
27
60
  // 1. CLI flag
28
61
  if (pkOverride)
29
62
  return pkOverride;
30
- // 2. Exchange-specific env vars
63
+ // 2. OWS active wallet (highest priority after explicit flags)
64
+ try {
65
+ const { loadSettings } = await import("./settings.js");
66
+ const settings = loadSettings();
67
+ if (settings.owsActiveWallet) {
68
+ const owsKey = resolveOwsWallet(settings.owsActiveWallet);
69
+ if (owsKey)
70
+ return owsKey;
71
+ }
72
+ }
73
+ catch {
74
+ // settings module not available, skip
75
+ }
76
+ // 3. Exchange-specific env vars
31
77
  const envMap = {
32
78
  pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
33
79
  hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
@@ -38,10 +84,10 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
38
84
  if (process.env[envVar])
39
85
  return process.env[envVar];
40
86
  }
41
- // 3. Generic fallback
87
+ // 4. Generic fallback
42
88
  if (process.env.PRIVATE_KEY)
43
89
  return process.env.PRIVATE_KEY;
44
- // 4. Active wallet from wallets.json
90
+ // 5. Legacy: Active wallet from wallets.json
45
91
  try {
46
92
  const { getActiveWalletKey } = await import("./commands/wallet.js");
47
93
  const walletKey = getActiveWalletKey(exchange);
@@ -56,12 +102,12 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
56
102
  if (existsSync(keyFile)) {
57
103
  return readFileSync(keyFile, "utf-8").trim();
58
104
  }
59
- throw new Error(`No private key configured for ${exchange}.\n\n` +
60
- ` Quick start: ${chalk.cyan("perp setup")}\n\n` +
105
+ throw new Error(`No wallet configured for ${exchange}.\n\n` +
106
+ ` Quick start: ${chalk.cyan("perp wallet generate")}\n\n` +
61
107
  ` Or manually:\n` +
62
- ` perp wallet set ${exchange} <key>\n` +
63
- ` ${envMap[exchange][0]}=<key> (env var)\n` +
64
- ` --private-key <key> (per-command)`);
108
+ ` perp wallet import <key> Import key to OWS vault\n` +
109
+ ` perp wallet set ${exchange} <key> Legacy: set env var\n` +
110
+ ` --private-key <key> Per-command override`);
65
111
  }
66
112
  export function parseSolanaKeypair(input) {
67
113
  // Try base58 first
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
+ }