@lifi/perps-sdk-provider-lighter 20.0.0 → 22.0.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 (135) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +70 -24
  3. package/dist/cjs/LighterProvider.js.map +1 -1
  4. package/dist/cjs/constants.d.ts +1 -0
  5. package/dist/cjs/constants.d.ts.map +1 -1
  6. package/dist/cjs/constants.js +2 -1
  7. package/dist/cjs/constants.js.map +1 -1
  8. package/dist/cjs/index.d.ts +1 -1
  9. package/dist/cjs/index.d.ts.map +1 -1
  10. package/dist/cjs/index.js +2 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  13. package/dist/cjs/signers/LighterReadOnlyTokenManager.js +1 -0
  14. package/dist/cjs/signers/LighterReadOnlyTokenManager.js.map +1 -1
  15. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  16. package/dist/cjs/signers/signActions.js +13 -5
  17. package/dist/cjs/signers/signActions.js.map +1 -1
  18. package/dist/cjs/types/account.d.ts +8 -3
  19. package/dist/cjs/types/account.d.ts.map +1 -1
  20. package/dist/cjs/types/activity.d.ts +7 -6
  21. package/dist/cjs/types/activity.d.ts.map +1 -1
  22. package/dist/cjs/types/fill.d.ts +3 -1
  23. package/dist/cjs/types/fill.d.ts.map +1 -1
  24. package/dist/cjs/types/order.d.ts +6 -1
  25. package/dist/cjs/types/order.d.ts.map +1 -1
  26. package/dist/cjs/utils/apiClient.d.ts +2 -1
  27. package/dist/cjs/utils/apiClient.d.ts.map +1 -1
  28. package/dist/cjs/utils/apiClient.js +28 -12
  29. package/dist/cjs/utils/apiClient.js.map +1 -1
  30. package/dist/cjs/utils/fetchDetailedAccount.d.ts +2 -2
  31. package/dist/cjs/utils/fetchDetailedAccount.d.ts.map +1 -1
  32. package/dist/cjs/utils/fetchDetailedAccount.js +9 -3
  33. package/dist/cjs/utils/fetchDetailedAccount.js.map +1 -1
  34. package/dist/cjs/utils/lighterErrorCode.d.ts +3 -0
  35. package/dist/cjs/utils/lighterErrorCode.d.ts.map +1 -0
  36. package/dist/cjs/utils/lighterErrorCode.js +17 -0
  37. package/dist/cjs/utils/lighterErrorCode.js.map +1 -0
  38. package/dist/cjs/utils/ohlcvInterval.d.ts.map +1 -1
  39. package/dist/cjs/utils/ohlcvInterval.js +0 -1
  40. package/dist/cjs/utils/ohlcvInterval.js.map +1 -1
  41. package/dist/cjs/utils/wireList.d.ts +2 -0
  42. package/dist/cjs/utils/wireList.d.ts.map +1 -0
  43. package/dist/cjs/utils/wireList.js +6 -0
  44. package/dist/cjs/utils/wireList.js.map +1 -0
  45. package/dist/esm/LighterProvider.d.ts.map +1 -1
  46. package/dist/esm/LighterProvider.js +91 -32
  47. package/dist/esm/LighterProvider.js.map +1 -1
  48. package/dist/esm/constants.d.ts +14 -2
  49. package/dist/esm/constants.d.ts.map +1 -1
  50. package/dist/esm/constants.js +14 -2
  51. package/dist/esm/constants.js.map +1 -1
  52. package/dist/esm/index.d.ts +1 -1
  53. package/dist/esm/index.d.ts.map +1 -1
  54. package/dist/esm/index.js +1 -1
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  57. package/dist/esm/signers/LighterReadOnlyTokenManager.js +3 -0
  58. package/dist/esm/signers/LighterReadOnlyTokenManager.js.map +1 -1
  59. package/dist/esm/signers/signActions.d.ts.map +1 -1
  60. package/dist/esm/signers/signActions.js +22 -8
  61. package/dist/esm/signers/signActions.js.map +1 -1
  62. package/dist/esm/types/account.d.ts +15 -3
  63. package/dist/esm/types/account.d.ts.map +1 -1
  64. package/dist/esm/types/account.js +1 -0
  65. package/dist/esm/types/account.js.map +1 -1
  66. package/dist/esm/types/activity.d.ts +8 -6
  67. package/dist/esm/types/activity.d.ts.map +1 -1
  68. package/dist/esm/types/activity.js +1 -0
  69. package/dist/esm/types/activity.js.map +1 -1
  70. package/dist/esm/types/fill.d.ts +5 -1
  71. package/dist/esm/types/fill.d.ts.map +1 -1
  72. package/dist/esm/types/fill.js +1 -0
  73. package/dist/esm/types/fill.js.map +1 -1
  74. package/dist/esm/types/order.d.ts +15 -1
  75. package/dist/esm/types/order.d.ts.map +1 -1
  76. package/dist/esm/types/order.js +1 -0
  77. package/dist/esm/types/order.js.map +1 -1
  78. package/dist/esm/utils/apiClient.d.ts +23 -15
  79. package/dist/esm/utils/apiClient.d.ts.map +1 -1
  80. package/dist/esm/utils/apiClient.js +55 -26
  81. package/dist/esm/utils/apiClient.js.map +1 -1
  82. package/dist/esm/utils/fetchDetailedAccount.d.ts +3 -2
  83. package/dist/esm/utils/fetchDetailedAccount.d.ts.map +1 -1
  84. package/dist/esm/utils/fetchDetailedAccount.js +10 -3
  85. package/dist/esm/utils/fetchDetailedAccount.js.map +1 -1
  86. package/dist/esm/utils/lighterErrorCode.d.ts +7 -0
  87. package/dist/esm/utils/lighterErrorCode.d.ts.map +1 -0
  88. package/dist/esm/utils/lighterErrorCode.js +26 -0
  89. package/dist/esm/utils/lighterErrorCode.js.map +1 -0
  90. package/dist/esm/utils/ohlcvInterval.d.ts.map +1 -1
  91. package/dist/esm/utils/ohlcvInterval.js +1 -2
  92. package/dist/esm/utils/ohlcvInterval.js.map +1 -1
  93. package/dist/esm/utils/wireList.d.ts +10 -0
  94. package/dist/esm/utils/wireList.d.ts.map +1 -0
  95. package/dist/esm/utils/wireList.js +10 -0
  96. package/dist/esm/utils/wireList.js.map +1 -0
  97. package/dist/types/LighterProvider.d.ts.map +1 -1
  98. package/dist/types/constants.d.ts +14 -2
  99. package/dist/types/constants.d.ts.map +1 -1
  100. package/dist/types/index.d.ts +1 -1
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/dist/types/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  103. package/dist/types/signers/signActions.d.ts.map +1 -1
  104. package/dist/types/types/account.d.ts +15 -3
  105. package/dist/types/types/account.d.ts.map +1 -1
  106. package/dist/types/types/activity.d.ts +8 -6
  107. package/dist/types/types/activity.d.ts.map +1 -1
  108. package/dist/types/types/fill.d.ts +5 -1
  109. package/dist/types/types/fill.d.ts.map +1 -1
  110. package/dist/types/types/order.d.ts +15 -1
  111. package/dist/types/types/order.d.ts.map +1 -1
  112. package/dist/types/utils/apiClient.d.ts +23 -15
  113. package/dist/types/utils/apiClient.d.ts.map +1 -1
  114. package/dist/types/utils/fetchDetailedAccount.d.ts +3 -2
  115. package/dist/types/utils/fetchDetailedAccount.d.ts.map +1 -1
  116. package/dist/types/utils/lighterErrorCode.d.ts +7 -0
  117. package/dist/types/utils/lighterErrorCode.d.ts.map +1 -0
  118. package/dist/types/utils/ohlcvInterval.d.ts.map +1 -1
  119. package/dist/types/utils/wireList.d.ts +10 -0
  120. package/dist/types/utils/wireList.d.ts.map +1 -0
  121. package/package.json +3 -3
  122. package/src/LighterProvider.ts +153 -62
  123. package/src/constants.ts +15 -2
  124. package/src/index.ts +1 -0
  125. package/src/signers/LighterReadOnlyTokenManager.ts +3 -0
  126. package/src/signers/signActions.ts +28 -11
  127. package/src/types/account.ts +17 -3
  128. package/src/types/activity.ts +9 -6
  129. package/src/types/fill.ts +6 -1
  130. package/src/types/order.ts +17 -1
  131. package/src/utils/apiClient.ts +68 -25
  132. package/src/utils/fetchDetailedAccount.ts +13 -6
  133. package/src/utils/lighterErrorCode.ts +30 -0
  134. package/src/utils/ohlcvInterval.ts +1 -2
  135. package/src/utils/wireList.ts +9 -0
@@ -1,5 +1,6 @@
1
1
  // Activity history shapes returned by Lighter's REST API
2
2
  // (deposits, withdrawals, funding payments, liquidations, transfers).
3
+ // Lighter serializes an empty list as JSON `null`, so list members are nullable.
3
4
 
4
5
  import type { LtAccountPosition } from './account.js'
5
6
 
@@ -27,7 +28,7 @@ export interface LtDepositHistoryItem {
27
28
  */
28
29
  export interface LtDepositHistoryResponse {
29
30
  code: number
30
- deposits: LtDepositHistoryItem[]
31
+ deposits: LtDepositHistoryItem[] | null
31
32
  cursor?: string
32
33
  }
33
34
 
@@ -56,7 +57,7 @@ export interface LtWithdrawHistoryItem {
56
57
  */
57
58
  export interface LtWithdrawHistoryResponse {
58
59
  code: number
59
- withdraws: LtWithdrawHistoryItem[]
60
+ withdraws: LtWithdrawHistoryItem[] | null
60
61
  cursor?: string
61
62
  }
62
63
 
@@ -80,6 +81,8 @@ export interface LtPositionFunding {
80
81
  rate: string
81
82
  position_size: string
82
83
  position_side: string
84
+ /** Funding-fee discount Lighter applied to `change`, as a decimal string. */
85
+ discount?: string
83
86
  }
84
87
 
85
88
  /**
@@ -90,7 +93,7 @@ export interface LtPositionFunding {
90
93
  */
91
94
  export interface LtPositionFundingsResponse {
92
95
  code: number
93
- position_fundings: LtPositionFunding[]
96
+ position_fundings: LtPositionFunding[] | null
94
97
  next_cursor?: string
95
98
  }
96
99
 
@@ -142,7 +145,7 @@ export type LtLiquidationPosition = Pick<
142
145
  * @public
143
146
  */
144
147
  export interface LtLiquidationInfo {
145
- positions: LtLiquidationPosition[]
148
+ positions: LtLiquidationPosition[] | null
146
149
  risk_info_before: {
147
150
  // Lighter's OpenAPI does not require this member, so an account without
148
151
  // cross exposure may omit it.
@@ -176,7 +179,7 @@ export interface LtLiquidation {
176
179
  */
177
180
  export interface LtLiquidationsResponse {
178
181
  code: number
179
- liquidations: LtLiquidation[]
182
+ liquidations: LtLiquidation[] | null
180
183
  next_cursor?: string
181
184
  }
182
185
 
@@ -212,6 +215,6 @@ export interface LtTransfer {
212
215
  */
213
216
  export interface LtTransferHistoryResponse {
214
217
  code: number
215
- transfers: LtTransfer[]
218
+ transfers: LtTransfer[] | null
216
219
  cursor?: string
217
220
  }
package/src/types/fill.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Trade (fill) shapes returned by Lighter's `/api/v1/trades` endpoint.
2
+ // Lighter serializes an empty list as JSON `null`, so list members are nullable.
2
3
 
3
4
  /**
4
5
  * Public trade/fill row returned by Lighter's `/api/v1/trades` endpoint.
@@ -54,6 +55,10 @@ export type LtTrade = {
54
55
  // older trade rows that predate the field.
55
56
  taker_initial_margin_fraction_before?: number
56
57
  maker_initial_margin_fraction_before?: number
58
+ /** Version of the resting ask order the trade matched against. */
59
+ ask_order_version?: number
60
+ /** Version of the resting bid order the trade matched against. */
61
+ bid_order_version?: number
57
62
  }
58
63
 
59
64
  /**
@@ -65,5 +70,5 @@ export type LtTrade = {
65
70
  export interface LtTradesResponse {
66
71
  code: number
67
72
  next_cursor: string
68
- trades: LtTrade[]
73
+ trades: LtTrade[] | null
69
74
  }
@@ -1,4 +1,5 @@
1
1
  // Order shapes returned by Lighter's REST API.
2
+ // Lighter serializes an empty list as JSON `null`, so list members are nullable.
2
3
 
3
4
  /**
4
5
  * Order payload returned by Lighter's REST API. Amounts and prices are decimal
@@ -46,6 +47,8 @@ export type LtOrder = {
46
47
  timestamp: number
47
48
  created_at: number
48
49
  updated_at: number
50
+ /** Lighter bit flags recording the order's wire options. */
51
+ order_flags?: number
49
52
  /**
50
53
  * Unix timestamp in microseconds. Lighter documents no unit, and its public
51
54
  * trade rows report this member in microseconds while the sibling
@@ -63,7 +66,20 @@ export type LtOrder = {
63
66
  export interface LtOrdersResponse {
64
67
  code: number
65
68
  next_cursor: string
66
- orders: LtOrder[]
69
+ orders: LtOrder[] | null
70
+ }
71
+
72
+ /**
73
+ * Order-lookup response from Lighter's `accountOrders` path. The lookup takes
74
+ * up to 20 `client_order_indexes` rather than a page, so it carries no cursor.
75
+ * It reaches the last 10K active orders with no time bound, and the last 1K
76
+ * inactive orders from the past 24 hours.
77
+ *
78
+ * @public
79
+ */
80
+ export interface LtAccountOrdersResponse {
81
+ code: number
82
+ orders: LtOrder[] | null
67
83
  }
68
84
 
69
85
  /**
@@ -9,6 +9,7 @@ import {
9
9
  LIGHTER_SUCCESS_CODES,
10
10
  LIGHTER_TOKEN_REVOKED_CODE,
11
11
  } from '../constants.js'
12
+ import { lighterErrorCodeFromBody } from './lighterErrorCode.js'
12
13
 
13
14
  /** @internal */
14
15
  export type ApiParams = Record<string, string | number | boolean>
@@ -48,6 +49,28 @@ const isLighterAuthRejection = (status: number, data: unknown): boolean =>
48
49
  const isLighterTokenRevoked = (data: unknown): boolean =>
49
50
  lighterBodyErrorCode(data) === LIGHTER_TOKEN_REVOKED_CODE
50
51
 
52
+ /**
53
+ * A CR or LF in a token would split the request when it reaches a
54
+ * `fetchImpl` that does not validate header values itself. A blank token
55
+ * would dispatch an `Authorization` header that carries no credential at
56
+ * all, which Lighter answers as a generic rejection.
57
+ */
58
+ const assertHeaderSafe = (authToken: string): string => {
59
+ if (authToken.trim().length === 0) {
60
+ throw new PerpsError(
61
+ PerpsErrorCode.ValidationError,
62
+ 'Lighter auth token is blank and cannot go in a header'
63
+ )
64
+ }
65
+ if (/[\r\n]/.test(authToken)) {
66
+ throw new PerpsError(
67
+ PerpsErrorCode.ValidationError,
68
+ 'Lighter auth token carries a line break and cannot go in a header'
69
+ )
70
+ }
71
+ return authToken
72
+ }
73
+
51
74
  /** @internal */
52
75
  export const LIGHTER_RETRY_DEFAULTS: ResolvedRetryPolicy = {
53
76
  enabled: true,
@@ -115,9 +138,17 @@ export interface LighterApiClientOptions {
115
138
  *
116
139
  * Auth-gated endpoints (accountLimits, accountActiveOrders, deposit/history,
117
140
  * withdraw/history, positionFunding, liquidations, transfer/history) take the
118
- * Lighter read-only token as the `auth` query parameter — NOT as an
119
- * `Authorization` header. This matches Lighter's OpenAPI spec and lets the
120
- * same call work browser-direct and from server-side proxies.
141
+ * Lighter read-only token in the `Authorization` header, which is the only
142
+ * auth channel Lighter's OpenAPI spec declares. The header makes such a read
143
+ * CORS-preflighted; both Lighter hosts answer the `OPTIONS` probe with
144
+ * `Authorization` in `Access-Control-Allow-Headers` and
145
+ * `Access-Control-Max-Age: 86400`, so a browser sends one preflight per day
146
+ * per origin.
147
+ *
148
+ * The `/api/v1/changeAccountTier` and `/api/v1/referral/use` mutations send
149
+ * the same header with a fresh per-call token. Their `OPTIONS` response is
150
+ * not yet probed against a live host, so a browser may reject the preflight
151
+ * even though the GET paths pass.
121
152
  *
122
153
  * Lighter signals rate limits through HTTP 429 or HTTP 405. This client never
123
154
  * retries such a response. It throws `RateLimitExceeded` and holds all network
@@ -198,29 +229,24 @@ export class LighterApiClient {
198
229
  return this.getChecked<T>(path, params)
199
230
  }
200
231
 
201
- /**
202
- * Auth-gated GET. The token is appended as the `auth` query parameter (per
203
- * Lighter's OpenAPI spec); the `Authorization` header is intentionally NOT
204
- * used — Lighter rejects it.
205
- */
232
+ /** Auth-gated GET carrying the Lighter token in the `Authorization` header. */
206
233
  async getAuthed<T>(
207
234
  path: string,
208
235
  authToken: string,
209
236
  params: ApiParams = {}
210
237
  ): Promise<T> {
211
- const { status, data } = await this.getWithStatus<unknown>(path, {
212
- ...params,
213
- auth: authToken,
238
+ const { status, data } = await this.sendGet<unknown>(path, params, {
239
+ Authorization: assertHeaderSafe(authToken),
214
240
  })
215
241
  if (isLighterTokenRevoked(data)) {
216
242
  throw new LighterTokenRevokedError(
217
- PerpsErrorCode.ThirdPartyError,
243
+ PerpsErrorCode.Unauthorized,
218
244
  `Lighter reports a revoked auth token for ${path}`
219
245
  )
220
246
  }
221
247
  if (isLighterAuthRejection(status, data)) {
222
248
  throw new LighterAuthRejectedError(
223
- PerpsErrorCode.ThirdPartyError,
249
+ PerpsErrorCode.Unauthorized,
224
250
  `Lighter rejected the auth token for ${path}`
225
251
  )
226
252
  }
@@ -236,12 +262,20 @@ export class LighterApiClient {
236
262
  async getWithStatus<T>(
237
263
  path: string,
238
264
  params?: ApiParams
265
+ ): Promise<{ status: number; data: T }> {
266
+ return this.sendGet<T>(path, params)
267
+ }
268
+
269
+ private async sendGet<T>(
270
+ path: string,
271
+ params?: ApiParams,
272
+ headers?: Record<string, string>
239
273
  ): Promise<{ status: number; data: T }> {
240
274
  this.assertRequestAllowed()
241
275
  const url = this.buildUrl(path, params)
242
276
  const response = await fetchWithRetry(
243
277
  url,
244
- {},
278
+ headers === undefined ? {} : { headers },
245
279
  {
246
280
  policy: this.policy,
247
281
  fetchImpl: this.fetchWithHold,
@@ -254,13 +288,15 @@ export class LighterApiClient {
254
288
  }
255
289
 
256
290
  /**
257
- * Form-encoded POST to a Lighter mutation endpoint. Single-shot — never
258
- * retried, since these are money/state writes whose outcome is unknown on a
259
- * transport failure. Surfaces the raw `{status, body}` pair so the caller can
260
- * map Lighter's per-endpoint business-rule `code` to a domain error verbatim.
291
+ * Form-encoded POST to a Lighter mutation endpoint, carrying the Lighter
292
+ * token in the `Authorization` header. Single-shot — never retried, since
293
+ * these are money/state writes whose outcome is unknown on a transport
294
+ * failure. Surfaces the raw `{status, body}` pair so the caller can map
295
+ * Lighter's per-endpoint business-rule `code` to a domain error verbatim.
261
296
  */
262
297
  async postForm<T>(
263
298
  path: string,
299
+ authToken: string,
264
300
  params: ApiParams
265
301
  ): Promise<{ status: number; data: T }> {
266
302
  const body = new URLSearchParams()
@@ -269,7 +305,10 @@ export class LighterApiClient {
269
305
  }
270
306
  const response = await this.fetchWithHold(`${this.baseUrl}${path}`, {
271
307
  method: 'POST',
272
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
308
+ headers: {
309
+ 'content-type': 'application/x-www-form-urlencoded',
310
+ Authorization: assertHeaderSafe(authToken),
311
+ },
273
312
  body: body.toString(),
274
313
  signal: this.signal,
275
314
  })
@@ -279,27 +318,31 @@ export class LighterApiClient {
279
318
  }
280
319
 
281
320
  private async getChecked<T>(path: string, params?: ApiParams): Promise<T> {
282
- const { status, data } = await this.getWithStatus<unknown>(path, params)
321
+ const { status, data } = await this.sendGet<unknown>(path, params)
283
322
  this.assertOk(path, status, data)
284
323
  return data as T
285
324
  }
286
325
 
287
326
  /**
288
327
  * Post-parse validation shared by every checked read: rejects a non-2xx HTTP
289
- * status and a 200 body carrying a non-success `code`. Callers that surface a
290
- * distinct auth-rejection error must run that check before this one.
328
+ * status and a 200 body carrying a non-success `code`. A body `code` that
329
+ * names a nonce, margin, or balance rejection sets the matching
330
+ * `PerpsErrorCode`; every other rejection is `ThirdPartyError`. Callers that
331
+ * surface a distinct auth-rejection error must run that check before this one.
291
332
  */
292
333
  private assertOk(path: string, status: number, data: unknown): void {
334
+ const errorCode = lighterBodyErrorCode(data)
335
+ const code =
336
+ lighterErrorCodeFromBody(errorCode) ?? PerpsErrorCode.ThirdPartyError
293
337
  if (status < 200 || status >= 300) {
294
338
  throw new PerpsError(
295
- PerpsErrorCode.ThirdPartyError,
339
+ code,
296
340
  `Lighter API request failed: ${status} — ${JSON.stringify(data).slice(0, 200)}`
297
341
  )
298
342
  }
299
- const errorCode = lighterBodyErrorCode(data)
300
343
  if (errorCode !== undefined) {
301
344
  throw new PerpsError(
302
- PerpsErrorCode.ThirdPartyError,
345
+ code,
303
346
  `Lighter API error for ${path}: code ${errorCode} — ${JSON.stringify(data).slice(0, 200)}`
304
347
  )
305
348
  }
@@ -2,22 +2,24 @@ import { PerpsError } from '@lifi/perps-sdk'
2
2
  import { PerpsErrorCode } from '@lifi/perps-types'
3
3
  import type { Address } from 'viem'
4
4
  import { LIGHTER_CODE_ACCOUNT_NOT_FOUND } from '../constants.js'
5
- import type { LtDetailedAccount } from '../types/index.js'
5
+ import type { LtAccount, LtDetailedAccount } from '../types/index.js'
6
6
  import type { LighterApiClient } from './apiClient.js'
7
+ import { wireList } from './wireList.js'
7
8
 
8
9
  /**
9
10
  * Look up the Lighter account for an L1 address via `/api/v1/account`,
10
11
  * discriminating Lighter's account-not-found body code from generic failures.
12
+ * The returned account carries real arrays for `positions` and `assets`.
11
13
  *
12
14
  * @public
13
15
  */
14
16
  export const fetchDetailedAccount = async (
15
17
  client: LighterApiClient,
16
18
  address: Address
17
- ): Promise<LtDetailedAccount> => {
19
+ ): Promise<LtAccount> => {
18
20
  const { status, data } = await client.getWithStatus<{
19
21
  code: number
20
- accounts?: LtDetailedAccount[]
22
+ accounts?: LtDetailedAccount[] | null
21
23
  message?: string
22
24
  }>('/api/v1/account', { by: 'l1_address', value: address })
23
25
 
@@ -35,12 +37,17 @@ export const fetchDetailedAccount = async (
35
37
  )
36
38
  }
37
39
 
38
- const accounts = data?.accounts
39
- if (accounts === undefined || accounts.length === 0) {
40
+ const accounts = wireList(data?.accounts)
41
+ const account = accounts[0]
42
+ if (account === undefined) {
40
43
  throw new PerpsError(
41
44
  PerpsErrorCode.AccountNotFound,
42
45
  `No Lighter account found for address: ${address}`
43
46
  )
44
47
  }
45
- return accounts[0]
48
+ return {
49
+ ...account,
50
+ positions: wireList(account.positions),
51
+ assets: wireList(account.assets),
52
+ }
46
53
  }
@@ -0,0 +1,30 @@
1
+ import { PerpsErrorCode } from '@lifi/perps-types'
2
+
3
+ /** Lighter body `code` values that name a rejection a `PerpsErrorCode` describes. */
4
+ const LIGHTER_BODY_ERROR_CODES: ReadonlyMap<number, PerpsErrorCode> = new Map([
5
+ // invalid nonce
6
+ [21104, PerpsErrorCode.InvalidNonce],
7
+ // batch transaction nonce is not increasing
8
+ [21105, PerpsErrorCode.InvalidNonce],
9
+ // not enough collateral
10
+ [21301, PerpsErrorCode.InsufficientBalance],
11
+ // not enough asset balance
12
+ [21304, PerpsErrorCode.InsufficientBalance],
13
+ // not enough asset balance for fee
14
+ [21305, PerpsErrorCode.InsufficientBalance],
15
+ // account is below maintenance margin
16
+ [21507, PerpsErrorCode.InsufficientMargin],
17
+ // account is below initial margin
18
+ [21508, PerpsErrorCode.InsufficientMargin],
19
+ // not enough margin to create the order
20
+ [21739, PerpsErrorCode.InsufficientMargin],
21
+ ])
22
+
23
+ /**
24
+ * Maps a Lighter body error `code` to the `PerpsErrorCode` it names. Returns
25
+ * `undefined` for every other code so the status-driven layer decides.
26
+ */
27
+ export const lighterErrorCodeFromBody = (
28
+ code: number | undefined
29
+ ): PerpsErrorCode | undefined =>
30
+ code === undefined ? undefined : LIGHTER_BODY_ERROR_CODES.get(code)
@@ -6,7 +6,7 @@ import { PerpsErrorCode } from '@lifi/perps-types'
6
6
  * Map our `OhlcvInterval` literal to Lighter's `resolution` enum.
7
7
  *
8
8
  * Lighter exposes only the timeframes listed below. SDK intervals without a
9
- * direct match (3m, 2h, 8h, 3d, 1M) raise a validation error rather than
9
+ * direct match (3m, 2h, 8h, 3d, 1w, 1M) raise a validation error rather than
10
10
  * silently rounding — the caller picks a supported timeframe.
11
11
  */
12
12
  const LIGHTER_SUPPORTED_INTERVALS: Partial<Record<OhlcvInterval, string>> = {
@@ -18,7 +18,6 @@ const LIGHTER_SUPPORTED_INTERVALS: Partial<Record<OhlcvInterval, string>> = {
18
18
  '4h': '4h',
19
19
  '12h': '12h',
20
20
  '1d': '1d',
21
- '1w': '1w',
22
21
  }
23
22
 
24
23
  /**
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Read a Lighter wire list member. Lighter serializes an empty list as JSON
3
+ * `null` rather than `[]`, so every list the SDK reads off a REST body passes
4
+ * through this at the response boundary and every mapper downstream works on a
5
+ * real array.
6
+ *
7
+ * @internal
8
+ */
9
+ export const wireList = <T>(value: T[] | null | undefined): T[] => value ?? []