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,4 +1,30 @@
1
1
  import { LocalEvmSigner } from "../signer/index.js";
2
+ import { isExpired } from "../agent-wallet/expiry.js";
3
+ import { PerpError } from "../errors.js";
4
+ /** Lighter trigger orderType enum (matches WASM signer). */
5
+ export const LIGHTER_ORDER_TYPE = {
6
+ STOP_LOSS: 2,
7
+ STOP_LOSS_LIMIT: 3,
8
+ TAKE_PROFIT: 4,
9
+ TAKE_PROFIT_LIMIT: 5,
10
+ };
11
+ /**
12
+ * Pick Lighter trigger orderType from side, trigger price, and current mark.
13
+ *
14
+ * STOP_LOSS protects against adverse moves; TAKE_PROFIT locks in favorable moves.
15
+ * buy + trigger >= mark → STOP_LOSS (close short on rise / breakout entry)
16
+ * buy + trigger < mark → TAKE_PROFIT (close short on fall)
17
+ * sell + trigger <= mark → STOP_LOSS (close long on fall)
18
+ * sell + trigger > mark → TAKE_PROFIT (close long on rise)
19
+ *
20
+ * Tied trigger==mark is classified as STOP_LOSS — the protective default.
21
+ */
22
+ export function classifyTriggerOrderType(side, triggerPrice, markPrice, isMarket) {
23
+ const isStopLoss = side === "buy" ? triggerPrice >= markPrice : triggerPrice <= markPrice;
24
+ if (isStopLoss)
25
+ return isMarket ? LIGHTER_ORDER_TYPE.STOP_LOSS : LIGHTER_ORDER_TYPE.STOP_LOSS_LIMIT;
26
+ return isMarket ? LIGHTER_ORDER_TYPE.TAKE_PROFIT : LIGHTER_ORDER_TYPE.TAKE_PROFIT_LIMIT;
27
+ }
2
28
  export class LighterAdapter {
3
29
  name = "lighter";
4
30
  chain = "ethereum";
@@ -21,6 +47,16 @@ export class LighterAdapter {
21
47
  _marketsCache = null;
22
48
  _marketsCacheTime = 0;
23
49
  static CACHE_TTL = 30_000;
50
+ // ── 3-tier signer state (Phase 2d) ──
51
+ /** Tier 1 metadata (Phase 2d). When set + not expired + !_useNoAgent, agent
52
+ * apiKeyIndex/publicKey/accountIndex take priority over master/PK env. */
53
+ _agentMeta;
54
+ /** Tier 1 secp256k1 hex private key (40 bytes) for the agent slot. Lighter
55
+ * has no OWS curve binding for raw L2 keys; this is held in-memory only when
56
+ * passed by the caller (e.g. immediately after `setupApiKey()` or via env). */
57
+ _agentApiKey;
58
+ /** --no-agent bypass flag. */
59
+ _useNoAgent = false;
24
60
  /**
25
61
  * @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
26
62
  * @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
@@ -28,9 +64,12 @@ export class LighterAdapter {
28
64
  */
29
65
  constructor(evmKey, testnet = false, opts) {
30
66
  this._evmKey = evmKey;
31
- this._apiKey = opts?.apiKey || process.env.LIGHTER_API_KEY || "";
32
- this._accountIndexInit = opts?.accountIndex ?? parseInt(process.env.LIGHTER_ACCOUNT_INDEX || "-1");
33
- this._apiKeyIndex = parseInt(process.env.LIGHTER_API_KEY_INDEX || "4");
67
+ // SSOT Rule #3: L2 API key is no longer read from env. Caller must inject
68
+ // via opts.apiKey (Tier 1 agent path uses setAgentSigner; auto-setup at
69
+ // init() runs ChangePubKey when apiKey is empty AND we have an EVM key).
70
+ this._apiKey = opts?.apiKey ?? "";
71
+ this._accountIndexInit = opts?.accountIndex ?? -1;
72
+ this._apiKeyIndex = opts?.apiKeyIndex ?? 4;
34
73
  this._address = "";
35
74
  this._testnet = testnet;
36
75
  this._readOnly = !this._apiKey;
@@ -64,19 +103,120 @@ export class LighterAdapter {
64
103
  /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
65
104
  setSigner(signer) {
66
105
  this._evmSigner = signer;
106
+ this._address = signer.getAddress();
107
+ // Reset any caller-supplied accountIndex hint: when the agent flow injects
108
+ // an OWS signer that differs from the constructor-time master, force a
109
+ // fresh /api/v1/account lookup keyed on the injected signer. Without this
110
+ // reset, a stale account index would route ChangePubKey to the wrong
111
+ // account (Lighter rejects the L1 sig with code=21504 "fail to l1 signature").
112
+ this._accountIndexInit = -1;
113
+ this._accountIndex = -1;
114
+ }
115
+ /**
116
+ * Tier 1: inject a Lighter agent's slot+L2 key (Phase 2d).
117
+ *
118
+ * Caller supplies the AgentMeta (settings.agents.lighter[name]) plus the
119
+ * 40-byte L2 private key (hex). Adapter swaps `_apiKey`/`_apiKeyIndex` to
120
+ * the agent's slot/key so all subsequent `signCreateOrder`/`signCancelOrder`
121
+ * etc. flow through the agent identity.
122
+ *
123
+ * NOTE: when the agent path is active, `_address` (master EVM) is left
124
+ * unchanged because Lighter REST queries (account, balance, positions) use
125
+ * `account_index` not address. The L1 EVM identity is irrelevant for the
126
+ * hot-path; only the L2 secp256k1 key is consulted.
127
+ */
128
+ setAgentSigner(meta, agentApiKey) {
129
+ this._agentMeta = meta;
130
+ this._agentApiKey = agentApiKey;
131
+ // Reset any prior accountIndex hint: the agent may belong to a different
132
+ // Lighter account than the master signer, so any stale value must not
133
+ // override the AgentMeta-derived accountIndex applied in init().
134
+ this._accountIndexInit = -1;
135
+ this._accountIndex = -1;
136
+ // Active slot+key swap happens lazily in `_resolveSigner()`/init flows.
137
+ }
138
+ /** Bypass Tier 1 (agent) — used for `--no-agent` flag. */
139
+ setNoAgent(noAgent) {
140
+ this._useNoAgent = noAgent;
141
+ }
142
+ /** Which signer tier is currently active (for debug/diagnostics). */
143
+ get activeSignerTier() {
144
+ try {
145
+ return this._resolveSigner().tier;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Resolve the active signer tier across three tiers (Phase 2d).
153
+ *
154
+ * Tier 1 (agent): active when AgentMeta + agent L2 key set, not expired,
155
+ * and `--no-agent` NOT set. The actual swap of `_apiKeyIndex`/`_apiKey`
156
+ * happens in `init()` (or via `_activateAgentTier()` post-init).
157
+ * Tier 2 (master): active when an OWS master EVM signer was injected via
158
+ * `setSigner()`. Used for the L1 ChangePubKey path (auto-setup at slot 4).
159
+ * Tier 3 (pk): active when constructor was supplied an evmKey. Same role
160
+ * as Tier 2 — signs L1 ChangePubKey for auto-setup.
161
+ *
162
+ * Lighter is unique among the 4 DEXs in that the actual order-signing is
163
+ * always done by the L2 secp256k1 key (`_apiKey`), regardless of tier. The
164
+ * tiers differ in WHICH L2 key is loaded: Tier 1 uses the agent's slot/key
165
+ * from AgentMeta; Tier 2/3 use whatever the env/auto-setup produced.
166
+ */
167
+ _resolveSigner() {
168
+ if (!this._useNoAgent && this._agentMeta && this._agentApiKey) {
169
+ if (isExpired(this._agentMeta)) {
170
+ // Skip Tier 1 only if Tier 2/3 available; otherwise throw AGENT_EXPIRED
171
+ if (!this._evmSigner && !this._evmKey) {
172
+ throw new PerpError("AGENT_EXPIRED", "Agent wallet has expired", {
173
+ remediation: "perp wallet agent approve lighter --rotate",
174
+ });
175
+ }
176
+ // fall through
177
+ }
178
+ else {
179
+ return { tier: "agent" };
180
+ }
181
+ }
182
+ if (this._evmSigner)
183
+ return { tier: "master" };
184
+ if (this._evmKey)
185
+ return { tier: "pk" };
186
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No signing path configured for Lighter", {
187
+ remediation: "Run one of: (a) perp wallet agent approve lighter --master <wallet>; (b) perp wallet generate && perp -e lighter --ows <wallet>; (c) export LIGHTER_PRIVATE_KEY=0x...",
188
+ });
67
189
  }
68
190
  async init() {
191
+ // Phase 2d: when an agent is registered (Tier 1), swap _apiKey/_apiKeyIndex
192
+ // to the agent's slot/key BEFORE any of the existing init logic runs. This
193
+ // ensures the WASM client below is created with the agent's credentials.
194
+ // The agent L2 key is loaded from the encrypted keystore by the caller and
195
+ // injected via setAgentSigner() — never read from env (SSOT Rule #3).
196
+ if (!this._useNoAgent && this._agentMeta && this._agentApiKey && !isExpired(this._agentMeta)) {
197
+ this._apiKey = this._agentApiKey;
198
+ if (typeof this._agentMeta.apiKeyIndex === "number") {
199
+ this._apiKeyIndex = this._agentMeta.apiKeyIndex;
200
+ }
201
+ if (typeof this._agentMeta.accountIndex === "number") {
202
+ this._accountIndex = this._agentMeta.accountIndex;
203
+ this._accountIndexInit = this._agentMeta.accountIndex;
204
+ }
205
+ }
69
206
  // Initialize EVM signer if not externally injected (skip if no key — read-only mode)
70
207
  if (!this._evmSigner && this._evmKey) {
71
208
  this._evmSigner = await LocalEvmSigner.create(this._evmKey);
72
209
  this._address = this._evmSigner.getAddress();
73
210
  }
211
+ else if (this._evmSigner && !this._address) {
212
+ this._address = this._evmSigner.getAddress();
213
+ }
74
214
  // Fetch account index from REST API (skip if no address — read-only mode)
75
215
  if (!this._address) {
76
216
  await this._refreshMarketMap();
77
217
  return;
78
218
  }
79
- // If accountIndex is already known (from env/opts), skip API lookup entirely
219
+ // If accountIndex is already known (from env/opts/agent meta), skip API lookup entirely
80
220
  if (this._accountIndexInit >= 0) {
81
221
  this._accountIndex = this._accountIndexInit;
82
222
  }
@@ -114,19 +254,18 @@ export class LighterAdapter {
114
254
  // setupApiKey already configured the static WASM client — reuse it
115
255
  this._signer = LighterAdapter._wasmClient;
116
256
  signerReady = true;
117
- // Save to .env for future use
257
+ // SSOT Rule #3: persist L2 key to encrypted keystore so subsequent
258
+ // process invocations can reload it without re-running ChangePubKey.
118
259
  try {
119
- const { setEnvVar } = await import("../commands/init.js");
120
- setEnvVar("LIGHTER_API_KEY", apiKey);
121
- setEnvVar("LIGHTER_ACCOUNT_INDEX", String(this._accountIndex));
122
- setEnvVar("LIGHTER_API_KEY_INDEX", String(autoKeyIndex));
260
+ const { saveLighterKey } = await import("../agent-wallet/lighter-keystore.js");
261
+ saveLighterKey(this._accountIndex, autoKeyIndex, apiKey, "");
123
262
  }
124
- catch { /* non-critical — env save may fail in some contexts */ }
263
+ catch { /* non-critical — keystore save may fail in some contexts */ }
125
264
  }
126
265
  catch (e) {
127
266
  // Auto-setup failed — log the error and continue in read-only mode
128
267
  const msg = e instanceof Error ? e.message : String(e);
129
- console.error(`[lighter] API key auto-setup failed: ${msg}. Trading will be read-only. Run 'perp -e lighter manage setup-api-key' to retry.`);
268
+ console.error(`[lighter] API key auto-setup failed: ${msg}. Trading will be read-only. Run 'perp wallet agent approve lighter' to register an agent.`);
130
269
  }
131
270
  }
132
271
  // Initialize signer for trading if we have an API key (reuse singleton WASM client)
@@ -136,7 +275,7 @@ export class LighterAdapter {
136
275
  if (cleanKey.startsWith("0x"))
137
276
  cleanKey = cleanKey.slice(2);
138
277
  if (cleanKey.length > 0 && !/^[0-9a-fA-F]+$/.test(cleanKey)) {
139
- console.error(`[lighter] API key contains invalid chars (expected hex). Check LIGHTER_API_KEY in ~/.perp/.env or run 'perp -e lighter manage setup-api-key' to regenerate.`);
278
+ console.error(`[lighter] API key contains invalid chars (expected hex). Run 'perp wallet agent approve lighter' to register an agent.`);
140
279
  this._apiKey = "";
141
280
  }
142
281
  else if (cleanKey.length === 0) {
@@ -144,7 +283,7 @@ export class LighterAdapter {
144
283
  }
145
284
  else if (this._accountIndex < 0) {
146
285
  // Account lookup failed or returned no accounts — cannot initialize WASM signer
147
- console.error(`[lighter] Account index not available (got ${this._accountIndex}). Trading will be read-only. Ensure the wallet has a Lighter account or set LIGHTER_ACCOUNT_INDEX.`);
286
+ console.error(`[lighter] Account index not available (got ${this._accountIndex}). Trading will be read-only. Ensure the wallet has a Lighter account, or run 'perp wallet agent approve lighter' to register an agent.`);
148
287
  this._apiKey = "";
149
288
  }
150
289
  else {
@@ -280,9 +419,10 @@ export class LighterAdapter {
280
419
  const maxLev = imf > 0 ? Math.floor(10000 / imf) : 50;
281
420
  const fr = fundingRates.get(d.symbol);
282
421
  const dec = this._marketDecimals.get(d.symbol.toUpperCase());
422
+ const fundingMark = fr && Number(fr.markPrice) > 0 ? fr.markPrice : null;
283
423
  markets.push({
284
424
  symbol: d.symbol,
285
- markPrice: fr?.markPrice ?? String(d.last_trade_price ?? 0),
425
+ markPrice: fundingMark ?? String(d.last_trade_price ?? 0),
286
426
  indexPrice: String(d.last_trade_price ?? 0),
287
427
  fundingRate: fr?.rate ?? null,
288
428
  volume24h: String(d.daily_quote_token_volume ?? 0),
@@ -616,28 +756,29 @@ export class LighterAdapter {
616
756
  const marketIndex = this.getMarketIndex(symbol);
617
757
  const { baseAmount } = this.toTicks(symbol, parseFloat(size), 0);
618
758
  const { priceTicks: triggerTicks } = this.toTicks(symbol, 0, parseFloat(triggerPrice));
619
- // If limitPrice given → stop-limit (type 0, GTT), else stop-market (type 1, IOC)
759
+ const triggerNum = parseFloat(triggerPrice);
760
+ const markPrice = await this.getMarkPrice(symbol);
620
761
  const isMarket = !opts?.limitPrice;
621
762
  let priceTicks;
622
763
  if (isMarket) {
623
- const markPrice = await this.getMarkPrice(symbol);
624
764
  const slippagePrice = side === "buy" ? markPrice * 2 : markPrice * 0.5;
625
765
  priceTicks = this.toTicks(symbol, 0, slippagePrice).priceTicks;
626
766
  }
627
767
  else {
628
768
  priceTicks = this.toTicks(symbol, 0, parseFloat(opts.limitPrice)).priceTicks;
629
769
  }
770
+ const orderType = classifyTriggerOrderType(side, triggerNum, markPrice, isMarket);
630
771
  const signed = await this.signOrder({
631
772
  marketIndex,
632
773
  clientOrderIndex: this.nextClientOrderIndex(),
633
774
  baseAmount,
634
775
  price: Math.max(priceTicks, 1),
635
776
  isAsk: side === "sell" ? 1 : 0,
636
- orderType: isMarket ? 1 : 0,
777
+ orderType,
637
778
  timeInForce: isMarket ? 0 : 1, // IOC for market, GTT for limit
638
779
  reduceOnly: opts?.reduceOnly ? 1 : 0,
639
780
  triggerPrice: triggerTicks,
640
- orderExpiry: isMarket ? 0 : -1,
781
+ orderExpiry: -1, // matches Lighter SDK DEFAULT_28_DAY_ORDER_EXPIRY for trigger orders
641
782
  nonce,
642
783
  });
643
784
  return this.sendTx(signed);
@@ -961,8 +1102,11 @@ export class LighterAdapter {
961
1102
  is_external_deposit: "true",
962
1103
  }),
963
1104
  });
964
- if (!res.ok)
965
- throw new Error(`createIntentAddress failed: ${await res.text()}`);
1105
+ if (!res.ok) {
1106
+ const text = await res.text();
1107
+ const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
1108
+ throw new Error(`createIntentAddress failed: ${clean}`);
1109
+ }
966
1110
  return res.json();
967
1111
  }
968
1112
  /**
@@ -989,7 +1133,15 @@ export class LighterAdapter {
989
1133
  account_index: String(this._accountIndex),
990
1134
  api_key_index: String(this._apiKeyIndex),
991
1135
  });
992
- return res.nonce ?? res.next_nonce ?? 0;
1136
+ const nonce = res.nonce ?? res.next_nonce;
1137
+ // SSOT rule #2: refuse to silently default to nonce 0. A stale or
1138
+ // missing nonce will cause Lighter to reject the tx with cryptic errors
1139
+ // and the user has no way to diagnose the upstream failure.
1140
+ if (nonce === undefined || !Number.isFinite(nonce) || nonce < 0) {
1141
+ throw new Error(`Lighter /nextNonce returned no valid nonce for account_index=${this._accountIndex} ` +
1142
+ `api_key_index=${this._apiKeyIndex}; raw response=${JSON.stringify(res)}`);
1143
+ }
1144
+ return nonce;
993
1145
  }
994
1146
  async signOrder(params) {
995
1147
  try {
@@ -1017,11 +1169,18 @@ export class LighterAdapter {
1017
1169
  throw new Error(`Signer error: ${signed.error}`);
1018
1170
  if (!signed.txInfo)
1019
1171
  throw new Error("Signer returned empty txInfo");
1172
+ // SSOT rule #2: refuse to silently default txType to 0 — different tx
1173
+ // types (createOrder=14, cancelOrder=15, updateLeverage=20, etc.) need
1174
+ // different routing on Lighter's side, and a 0 either rejects with a
1175
+ // generic error or silently misroutes.
1176
+ if (signed.txType === undefined || !Number.isFinite(signed.txType)) {
1177
+ throw new Error(`Lighter signer returned no txType (received ${signed.txType}); refusing to send with default 0.`);
1178
+ }
1020
1179
  const res = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
1021
1180
  method: "POST",
1022
1181
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1023
1182
  body: new URLSearchParams({
1024
- tx_type: String(signed.txType ?? 0),
1183
+ tx_type: String(signed.txType),
1025
1184
  tx_info: signed.txInfo,
1026
1185
  }),
1027
1186
  });
@@ -1035,8 +1194,11 @@ export class LighterAdapter {
1035
1194
  for (const [k, v] of Object.entries(params))
1036
1195
  url.searchParams.set(k, v);
1037
1196
  const res = await fetch(url.toString());
1038
- if (!res.ok)
1039
- throw new Error(`GET ${path} failed (${res.status}): ${await res.text()}`);
1197
+ if (!res.ok) {
1198
+ const text = await res.text();
1199
+ const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
1200
+ throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
1201
+ }
1040
1202
  return res.json();
1041
1203
  }
1042
1204
  /** Generate a unique client order index (uint48). */
@@ -1046,8 +1208,7 @@ export class LighterAdapter {
1046
1208
  }
1047
1209
  ensureSigner() {
1048
1210
  if (this._readOnly || this._accountIndex < 0 || !this._signer) {
1049
- throw new Error("This command requires a Lighter API key. Run `perp -e lighter manage setup-api-key` first, " +
1050
- "then set LIGHTER_API_KEY in your .env");
1211
+ throw new Error("This command requires a Lighter API key. Run `perp wallet agent approve lighter` to register an agent.");
1051
1212
  }
1052
1213
  }
1053
1214
  static _wasmClient = null;
@@ -1098,16 +1259,22 @@ export class LighterAdapter {
1098
1259
  throw new Error("No EVM private key configured. Run: perp setup");
1099
1260
  }
1100
1261
  if (this._accountIndex < 0) {
1101
- throw new Error("Account index not available. Call init() first or set LIGHTER_ACCOUNT_INDEX.");
1262
+ throw new Error("Account index not available. Call init() first.");
1102
1263
  }
1103
1264
  // 1. Generate key pair
1104
1265
  const { privateKey, publicKey } = await LighterAdapter.generateApiKey();
1105
1266
  // 2. Get nonce from API
1267
+ // SSOT rule #2: refuse to silently default to nonce 0 — see getNextNonce.
1106
1268
  const nonceRes = await this.restGet("/nextNonce", {
1107
1269
  account_index: String(this._accountIndex),
1108
1270
  api_key_index: String(apiKeyIndex),
1109
1271
  });
1110
- const nonce = nonceRes.nonce ?? nonceRes.next_nonce ?? 0;
1272
+ const nonce = nonceRes.nonce ?? nonceRes.next_nonce;
1273
+ if (nonce === undefined || !Number.isFinite(nonce) || nonce < 0) {
1274
+ throw new Error(`Lighter /nextNonce returned no valid nonce for ChangePubKey ` +
1275
+ `(account_index=${this._accountIndex} api_key_index=${apiKeyIndex}); ` +
1276
+ `raw response=${JSON.stringify(nonceRes)}`);
1277
+ }
1111
1278
  // 3. Create signer client with new key and sign ChangePubKey
1112
1279
  const client = await LighterAdapter.getWasmClient();
1113
1280
  await client.createClient({
@@ -1135,11 +1302,15 @@ export class LighterAdapter {
1135
1302
  const txInfo = JSON.parse(signed.txInfo);
1136
1303
  txInfo.L1Sig = l1Sig;
1137
1304
  // 6. Submit to Lighter API
1305
+ // SSOT rule #2: refuse to silently default txType to 0 — see sendTx.
1306
+ if (signed.txType === undefined || !Number.isFinite(signed.txType)) {
1307
+ throw new Error(`Lighter signChangePubKey returned no txType (received ${signed.txType}); refusing to send with default 0.`);
1308
+ }
1138
1309
  const sendRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
1139
1310
  method: "POST",
1140
1311
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1141
1312
  body: new URLSearchParams({
1142
- tx_type: String(signed.txType ?? 0),
1313
+ tx_type: String(signed.txType),
1143
1314
  tx_info: JSON.stringify(txInfo),
1144
1315
  }),
1145
1316
  });
@@ -1160,7 +1331,14 @@ export class LighterAdapter {
1160
1331
  account_index: String(this._accountIndex),
1161
1332
  api_key_index: String(apiKeyIndex),
1162
1333
  });
1163
- const freshNonce = (freshNonceRes.nonce ?? freshNonceRes.next_nonce ?? nonce) + attempt;
1334
+ const freshNonceCandidate = freshNonceRes.nonce ?? freshNonceRes.next_nonce;
1335
+ if (freshNonceCandidate === undefined || !Number.isFinite(freshNonceCandidate) || freshNonceCandidate < 0) {
1336
+ // SSOT rule #2: refuse to silently fall back to the original nonce.
1337
+ // Skip the retry attempt and surface the upstream nonce-fetch failure.
1338
+ lastError = `fresh /nextNonce returned no valid nonce (raw=${JSON.stringify(freshNonceRes)})`;
1339
+ continue;
1340
+ }
1341
+ const freshNonce = freshNonceCandidate + attempt;
1164
1342
  const retrySigned = await client.signChangePubKey({
1165
1343
  pubkey: publicKey, nonce: freshNonce, apiKeyIndex, accountIndex: this._accountIndex,
1166
1344
  });
@@ -1168,13 +1346,17 @@ export class LighterAdapter {
1168
1346
  lastError = retrySigned.error ?? "incomplete response";
1169
1347
  continue;
1170
1348
  }
1349
+ if (retrySigned.txType === undefined || !Number.isFinite(retrySigned.txType)) {
1350
+ lastError = `signChangePubKey retry returned no txType (received ${retrySigned.txType})`;
1351
+ continue;
1352
+ }
1171
1353
  const retryTxInfo = JSON.parse(retrySigned.txInfo);
1172
1354
  retryTxInfo.L1Sig = await this._evmSigner.signMessage(retrySigned.messageToSign);
1173
1355
  const retryRes = await fetch(`${this._baseUrl}/api/v1/sendTx`, {
1174
1356
  method: "POST",
1175
1357
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1176
1358
  body: new URLSearchParams({
1177
- tx_type: String(retrySigned.txType ?? 0),
1359
+ tx_type: String(retrySigned.txType),
1178
1360
  tx_info: JSON.stringify(retryTxInfo),
1179
1361
  }),
1180
1362
  });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Pacifica agent-wallet signing builders (Phase 2c).
3
+ *
4
+ * Pacifica auth model — off-chain signed messages over Solana Ed25519.
5
+ * Reference: https://pacifica.gitbook.io/docs/api-documentation/api/signing.md
6
+ *
7
+ * Canonical JSON rules (from Pacifica signing spec + production reference
8
+ * `src/pacifica/signing.ts:prepareMessage()`):
9
+ * 1. Recursively sort all object keys alphabetically.
10
+ * 2. Compact JSON (`JSON.stringify(value)` — no whitespace).
11
+ * 3. Outer wrapper has the shape `{ <header fields>, "data": <payload> }`
12
+ * where header is `{ type, timestamp, expiry_window }`. The `data` key
13
+ * is sorted lexicographically alongside the header fields.
14
+ *
15
+ * The agent bind/unbind action types are documented in HypurrQuant_FE
16
+ * `PacificaPerpAdapter.ts` (production reference for HypurrQuant_FE; see
17
+ * project memory `hypurrquant_fe_reference.md`). We mirror their `bind_agent_wallet`
18
+ * and `unbind_agent_wallet` operation types here.
19
+ *
20
+ * Pure functions — no side effects, no fs/fetch/env reads.
21
+ */
22
+ /** Default expiry window for signed Pacifica REST requests (ms). */
23
+ export declare const DEFAULT_EXPIRY_WINDOW = 5000;
24
+ export interface BindAgentMessageParams {
25
+ /** Master Solana base58 public key. */
26
+ account: string;
27
+ /** Agent Solana base58 public key. */
28
+ agentWallet: string;
29
+ /** Optional ms-epoch timestamp; defaults to `Date.now()`. */
30
+ timestamp?: number;
31
+ /** Optional expiry window (ms); defaults to DEFAULT_EXPIRY_WINDOW. */
32
+ expiryWindow?: number;
33
+ }
34
+ export interface UnbindAgentMessageParams {
35
+ /** Master Solana base58 public key. */
36
+ account: string;
37
+ /**
38
+ * Agent Solana base58 public key to unbind. Pass an empty string to
39
+ * unbind all agent wallets per Pacifica convention.
40
+ */
41
+ agentWallet: string;
42
+ timestamp?: number;
43
+ expiryWindow?: number;
44
+ }
45
+ export interface BuiltMessage {
46
+ /** Canonical JSON string ready for Ed25519 signing. */
47
+ canonicalJson: string;
48
+ /** Header captured for use in the REST envelope. */
49
+ header: {
50
+ type: string;
51
+ timestamp: number;
52
+ expiry_window: number;
53
+ };
54
+ /** Original payload object (caller supplies in REST envelope alongside signature). */
55
+ payload: Record<string, unknown>;
56
+ }
57
+ /**
58
+ * Build the canonical JSON for `bind_agent_wallet`.
59
+ *
60
+ * Wire shape (after sorting):
61
+ * {"data":{"agent_wallet":"<base58>"},"expiry_window":5000,"timestamp":...,"type":"bind_agent_wallet"}
62
+ *
63
+ * The `account` field is supplied separately in the REST envelope; only the
64
+ * `agent_wallet` payload field is included inside the signed `data` block.
65
+ * This mirrors `buildSignedRequest()` in `src/pacifica/signing.ts`, which is
66
+ * the production reference for non-agent signed Pacifica requests.
67
+ */
68
+ export declare function buildBindAgentMessage(params: BindAgentMessageParams): BuiltMessage;
69
+ /**
70
+ * Build the canonical JSON for `unbind_agent_wallet`.
71
+ *
72
+ * Pacifica convention: empty `agent_wallet` string unbinds all agents; a
73
+ * specific base58 address unbinds just that agent.
74
+ */
75
+ export declare function buildUnbindAgentMessage(params: UnbindAgentMessageParams): BuiltMessage;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pacifica agent-wallet signing builders (Phase 2c).
3
+ *
4
+ * Pacifica auth model — off-chain signed messages over Solana Ed25519.
5
+ * Reference: https://pacifica.gitbook.io/docs/api-documentation/api/signing.md
6
+ *
7
+ * Canonical JSON rules (from Pacifica signing spec + production reference
8
+ * `src/pacifica/signing.ts:prepareMessage()`):
9
+ * 1. Recursively sort all object keys alphabetically.
10
+ * 2. Compact JSON (`JSON.stringify(value)` — no whitespace).
11
+ * 3. Outer wrapper has the shape `{ <header fields>, "data": <payload> }`
12
+ * where header is `{ type, timestamp, expiry_window }`. The `data` key
13
+ * is sorted lexicographically alongside the header fields.
14
+ *
15
+ * The agent bind/unbind action types are documented in HypurrQuant_FE
16
+ * `PacificaPerpAdapter.ts` (production reference for HypurrQuant_FE; see
17
+ * project memory `hypurrquant_fe_reference.md`). We mirror their `bind_agent_wallet`
18
+ * and `unbind_agent_wallet` operation types here.
19
+ *
20
+ * Pure functions — no side effects, no fs/fetch/env reads.
21
+ */
22
+ import { sortJsonKeys } from "../pacifica/signing.js";
23
+ /** Default expiry window for signed Pacifica REST requests (ms). */
24
+ export const DEFAULT_EXPIRY_WINDOW = 5_000;
25
+ // ── Builders ─────────────────────────────────────────────────────────────
26
+ /**
27
+ * Build the canonical JSON for `bind_agent_wallet`.
28
+ *
29
+ * Wire shape (after sorting):
30
+ * {"data":{"agent_wallet":"<base58>"},"expiry_window":5000,"timestamp":...,"type":"bind_agent_wallet"}
31
+ *
32
+ * The `account` field is supplied separately in the REST envelope; only the
33
+ * `agent_wallet` payload field is included inside the signed `data` block.
34
+ * This mirrors `buildSignedRequest()` in `src/pacifica/signing.ts`, which is
35
+ * the production reference for non-agent signed Pacifica requests.
36
+ */
37
+ export function buildBindAgentMessage(params) {
38
+ const timestamp = params.timestamp ?? Date.now();
39
+ const expiryWindow = params.expiryWindow ?? DEFAULT_EXPIRY_WINDOW;
40
+ const type = "bind_agent_wallet";
41
+ const payload = {
42
+ agent_wallet: params.agentWallet,
43
+ };
44
+ const header = { type, timestamp, expiry_window: expiryWindow };
45
+ const wrapped = { ...header, data: payload };
46
+ const sorted = sortJsonKeys(wrapped);
47
+ return {
48
+ canonicalJson: JSON.stringify(sorted),
49
+ header,
50
+ payload,
51
+ };
52
+ }
53
+ /**
54
+ * Build the canonical JSON for `unbind_agent_wallet`.
55
+ *
56
+ * Pacifica convention: empty `agent_wallet` string unbinds all agents; a
57
+ * specific base58 address unbinds just that agent.
58
+ */
59
+ export function buildUnbindAgentMessage(params) {
60
+ const timestamp = params.timestamp ?? Date.now();
61
+ const expiryWindow = params.expiryWindow ?? DEFAULT_EXPIRY_WINDOW;
62
+ const type = "unbind_agent_wallet";
63
+ const payload = {
64
+ agent_wallet: params.agentWallet,
65
+ };
66
+ const header = { type, timestamp, expiry_window: expiryWindow };
67
+ const wrapped = { ...header, data: payload };
68
+ const sorted = sortJsonKeys(wrapped);
69
+ return {
70
+ canonicalJson: JSON.stringify(sorted),
71
+ header,
72
+ payload,
73
+ };
74
+ }
@@ -2,22 +2,50 @@ import { Keypair } from "@solana/web3.js";
2
2
  import { PacificaClient, type Network } from "../pacifica/index.js";
3
3
  import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
4
4
  import type { SolanaSigner } from "../signer/index.js";
5
+ import type { AgentMeta } from "../settings.js";
5
6
  export declare class PacificaAdapter implements ExchangeAdapter {
6
7
  readonly name = "pacifica";
7
8
  readonly chain = "solana";
8
9
  readonly aliases: readonly ["pac"];
9
10
  private client;
11
+ /** Tier 3: PK direct (constructor-supplied keypair). */
10
12
  private _solanaSigner;
13
+ /** Tier 2: OWS master signer (set via setSigner). */
14
+ private _masterSigner?;
15
+ /** Tier 1: agent OWS signer (Phase 2c). */
16
+ private _agentSigner?;
17
+ /** Tier 1 metadata. */
18
+ private _agentMeta?;
19
+ /** --no-agent bypass flag. */
20
+ private _useNoAgent;
21
+ /** True when adapter was constructed without a real PK (read-only). */
11
22
  private _hasRealKey;
23
+ /** Public Solana base58 of the active signer (`_resolveSigner().address`). */
12
24
  private account;
25
+ /** Bound `signMessage` for the active signer (Phase 2c routes through `_resolveSigner`). */
13
26
  private signMessage;
14
27
  private _marketsCache;
15
28
  private _marketsCacheTime;
16
29
  private static readonly CACHE_TTL;
17
30
  constructor(keypair: Keypair, network?: Network, builderCode?: string, hasRealKey?: boolean);
18
31
  private ensureSigner;
19
- /** Inject an external Solana signer. */
32
+ /**
33
+ * Inject an external Solana signer (Tier 2 — OWS master).
34
+ * Backward-compatible: existing callers (e.g. `perp -e pacifica --ows ...`
35
+ * in `_initWithOws`) continue to work; the master signer is what
36
+ * `_resolveSigner()` falls back to when no agent is registered.
37
+ */
20
38
  setSigner(signer: SolanaSigner): void;
39
+ /** Tier 1: inject agent OWS Solana signer (Phase 2c). */
40
+ setAgentSigner(meta: AgentMeta, signer: SolanaSigner): void;
41
+ /** Bypass Tier 1 (agent) — used for `--no-agent` flag. */
42
+ setNoAgent(noAgent: boolean): void;
43
+ /** True only when ALL THREE tiers are unconfigured. */
44
+ get isReadOnly(): boolean;
45
+ /** Which signer tier is currently active (for debug/diagnostics). */
46
+ get activeSignerTier(): "agent" | "master" | "pk" | null;
47
+ /** Resolve the active signer across three tiers. Mirrors AsterAdapter._resolveSigner. */
48
+ private _resolveSigner;
21
49
  /** Access the underlying Keypair (only available with LocalSolanaSigner). */
22
50
  get keypair(): Keypair;
23
51
  private _getPrices;