@lifi/perps-sdk-provider-lighter 17.3.0 → 18.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 (69) hide show
  1. package/README.md +8 -0
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +76 -57
  4. package/dist/cjs/LighterProvider.js.map +1 -1
  5. package/dist/cjs/accountConfig.d.ts.map +1 -1
  6. package/dist/cjs/accountConfig.js.map +1 -1
  7. package/dist/cjs/constants.d.ts +2 -1
  8. package/dist/cjs/constants.d.ts.map +1 -1
  9. package/dist/cjs/constants.js +3 -1
  10. package/dist/cjs/constants.js.map +1 -1
  11. package/dist/cjs/signers/LighterKeyStore.d.ts +3 -1
  12. package/dist/cjs/signers/LighterKeyStore.d.ts.map +1 -1
  13. package/dist/cjs/signers/LighterKeyStore.js +14 -2
  14. package/dist/cjs/signers/LighterKeyStore.js.map +1 -1
  15. package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts +7 -0
  16. package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  17. package/dist/cjs/signers/LighterReadOnlyTokenManager.js +19 -7
  18. package/dist/cjs/signers/LighterReadOnlyTokenManager.js.map +1 -1
  19. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  20. package/dist/cjs/signers/signActions.js +8 -0
  21. package/dist/cjs/signers/signActions.js.map +1 -1
  22. package/dist/cjs/utils/apiClient.d.ts +12 -0
  23. package/dist/cjs/utils/apiClient.d.ts.map +1 -1
  24. package/dist/cjs/utils/apiClient.js +62 -7
  25. package/dist/cjs/utils/apiClient.js.map +1 -1
  26. package/dist/esm/LighterProvider.d.ts.map +1 -1
  27. package/dist/esm/LighterProvider.js +78 -102
  28. package/dist/esm/LighterProvider.js.map +1 -1
  29. package/dist/esm/accountConfig.d.ts.map +1 -1
  30. package/dist/esm/accountConfig.js +2 -4
  31. package/dist/esm/accountConfig.js.map +1 -1
  32. package/dist/esm/constants.d.ts +7 -1
  33. package/dist/esm/constants.d.ts.map +1 -1
  34. package/dist/esm/constants.js +7 -1
  35. package/dist/esm/constants.js.map +1 -1
  36. package/dist/esm/signers/LighterKeyStore.d.ts +4 -1
  37. package/dist/esm/signers/LighterKeyStore.d.ts.map +1 -1
  38. package/dist/esm/signers/LighterKeyStore.js +15 -3
  39. package/dist/esm/signers/LighterKeyStore.js.map +1 -1
  40. package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts +12 -0
  41. package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  42. package/dist/esm/signers/LighterReadOnlyTokenManager.js +23 -7
  43. package/dist/esm/signers/LighterReadOnlyTokenManager.js.map +1 -1
  44. package/dist/esm/signers/signActions.d.ts.map +1 -1
  45. package/dist/esm/signers/signActions.js +9 -0
  46. package/dist/esm/signers/signActions.js.map +1 -1
  47. package/dist/esm/utils/apiClient.d.ts +34 -7
  48. package/dist/esm/utils/apiClient.d.ts.map +1 -1
  49. package/dist/esm/utils/apiClient.js +77 -14
  50. package/dist/esm/utils/apiClient.js.map +1 -1
  51. package/dist/types/LighterProvider.d.ts.map +1 -1
  52. package/dist/types/accountConfig.d.ts.map +1 -1
  53. package/dist/types/constants.d.ts +7 -1
  54. package/dist/types/constants.d.ts.map +1 -1
  55. package/dist/types/signers/LighterKeyStore.d.ts +4 -1
  56. package/dist/types/signers/LighterKeyStore.d.ts.map +1 -1
  57. package/dist/types/signers/LighterReadOnlyTokenManager.d.ts +12 -0
  58. package/dist/types/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
  59. package/dist/types/signers/signActions.d.ts.map +1 -1
  60. package/dist/types/utils/apiClient.d.ts +34 -7
  61. package/dist/types/utils/apiClient.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/src/LighterProvider.ts +103 -138
  64. package/src/accountConfig.ts +2 -4
  65. package/src/constants.ts +8 -0
  66. package/src/signers/LighterKeyStore.ts +28 -3
  67. package/src/signers/LighterReadOnlyTokenManager.ts +39 -6
  68. package/src/signers/signActions.ts +10 -0
  69. package/src/utils/apiClient.ts +106 -13
@@ -1,5 +1,9 @@
1
- import { readValidatedRecord, type StorageAdapter } from '@lifi/perps-sdk'
2
- import type { LighterProviderKey } from '@lifi/perps-types'
1
+ import {
2
+ PerpsError,
3
+ readValidatedRecord,
4
+ type StorageAdapter,
5
+ } from '@lifi/perps-sdk'
6
+ import { type LighterProviderKey, PerpsErrorCode } from '@lifi/perps-types'
3
7
  import type { Address } from 'viem'
4
8
  import { LIGHTER_PROVIDER_KEY } from '../constants.js'
5
9
 
@@ -30,6 +34,8 @@ export interface LighterApiKey {
30
34
  apiKeyPrivateKey: string
31
35
  /** Corresponding public key, registered via ChangePubKey. */
32
36
  apiKeyPublicKey: string
37
+ /** Referral code confirmed by the standard-token SET_REFERRAL action. */
38
+ appliedReferralCode?: string
33
39
  }
34
40
 
35
41
  const isLighterApiKeyOf =
@@ -44,6 +50,7 @@ const isLighterApiKeyOf =
44
50
  apiKeyIndex,
45
51
  apiKeyPrivateKey,
46
52
  apiKeyPublicKey,
53
+ appliedReferralCode,
47
54
  } = value as Record<string, unknown>
48
55
  return (
49
56
  providerKey === owner &&
@@ -54,7 +61,10 @@ const isLighterApiKeyOf =
54
61
  typeof apiKeyPrivateKey === 'string' &&
55
62
  apiKeyPrivateKey.length > 0 &&
56
63
  typeof apiKeyPublicKey === 'string' &&
57
- apiKeyPublicKey.length > 0
64
+ apiKeyPublicKey.length > 0 &&
65
+ (appliedReferralCode === undefined ||
66
+ (typeof appliedReferralCode === 'string' &&
67
+ appliedReferralCode.length > 0))
58
68
  )
59
69
  }
60
70
 
@@ -140,6 +150,21 @@ export class LighterKeyStore {
140
150
  await this.storage.set(key, JSON.stringify(record))
141
151
  }
142
152
 
153
+ async markReferralApplied(
154
+ address: Address,
155
+ appliedReferralCode: string
156
+ ): Promise<void> {
157
+ const current = await this.get(address)
158
+ if (current === null) {
159
+ throw new PerpsError(
160
+ PerpsErrorCode.SDKError,
161
+ `Cannot persist the Lighter referral marker without an API key for ${address}`
162
+ )
163
+ }
164
+ const { providerKey: _providerKey, ...value } = current
165
+ await this.set(address, { ...value, appliedReferralCode })
166
+ }
167
+
143
168
  /**
144
169
  * Remove the persisted API key for an L1 address and clear its in-memory
145
170
  * cache entry.
@@ -75,6 +75,7 @@ export type LighterTokenFetcher = (params: {
75
75
  expiry: number
76
76
  subAccountAccess: boolean
77
77
  scopes: string
78
+ fetchImpl?: typeof fetch
78
79
  }) => Promise<LighterCreateTokenResponse>
79
80
 
80
81
  /**
@@ -127,6 +128,7 @@ export type LighterTokenListFetcher = (params: {
127
128
  url: string
128
129
  authorization: string
129
130
  accountIndex: number
131
+ fetchImpl?: typeof fetch
130
132
  }) => Promise<LighterListTokensResponse>
131
133
 
132
134
  /**
@@ -139,6 +141,7 @@ export type LighterTokenRevokeFetcher = (params: {
139
141
  authorization: string
140
142
  tokenId: number
141
143
  accountIndex: number
144
+ fetchImpl?: typeof fetch
142
145
  }) => Promise<LighterRevokeTokenResponse>
143
146
 
144
147
  /**
@@ -165,6 +168,8 @@ export interface LighterReadOnlyTokenManagerOptions {
165
168
  listFetcher?: LighterTokenListFetcher
166
169
  /** Override the revoke POST. Defaults to a `fetch`-based implementation. */
167
170
  revokeFetcher?: LighterTokenRevokeFetcher
171
+ /** Transport used by the default endpoint fetchers. Defaults to `fetch`. */
172
+ fetchImpl?: typeof fetch
168
173
  /** Clock injection for testing expiry logic. Defaults to `Date.now`. */
169
174
  now?: () => number
170
175
  }
@@ -259,6 +264,7 @@ export class LighterReadOnlyTokenManager {
259
264
  private readonly fetcher: LighterTokenFetcher
260
265
  private readonly listFetcher: LighterTokenListFetcher
261
266
  private readonly revokeFetcher: LighterTokenRevokeFetcher
267
+ private readonly fetchImpl: typeof fetch | undefined
262
268
  private readonly now: () => number
263
269
  private readonly cache = new Map<string, LighterReadOnlyToken>()
264
270
 
@@ -270,6 +276,7 @@ export class LighterReadOnlyTokenManager {
270
276
  this.listFetcher = options.listFetcher ?? defaultLighterTokenListFetcher
271
277
  this.revokeFetcher =
272
278
  options.revokeFetcher ?? defaultLighterTokenRevokeFetcher
279
+ this.fetchImpl = options.fetchImpl
273
280
  this.now = options.now ?? (() => Date.now())
274
281
  }
275
282
 
@@ -398,10 +405,31 @@ export class LighterReadOnlyTokenManager {
398
405
  authorization: string,
399
406
  inputs: ApproveReadOnlyTokenInputs
400
407
  ): Promise<ApproveReadOnlyTokenResult> {
401
- const { address, accountIndex, expirySeconds, scope } = inputs
408
+ await this.revokeStaleOwnTokens(
409
+ authorization,
410
+ inputs.address,
411
+ inputs.accountIndex
412
+ )
413
+ return this.createAndPersist(authorization, inputs)
414
+ }
402
415
 
403
- await this.revokeStaleOwnTokens(authorization, address, accountIndex)
416
+ /**
417
+ * Replace a token that Lighter explicitly reports as revoked.
418
+ * The venue already revoked the row, so this path skips registry cleanup.
419
+ */
420
+ async replaceRevoked(
421
+ authorization: string,
422
+ inputs: ApproveReadOnlyTokenInputs
423
+ ): Promise<ApproveReadOnlyTokenResult> {
424
+ await this.remove(inputs.address, inputs.accountIndex)
425
+ return this.createAndPersist(authorization, inputs)
426
+ }
404
427
 
428
+ private async createAndPersist(
429
+ authorization: string,
430
+ inputs: ApproveReadOnlyTokenInputs
431
+ ): Promise<ApproveReadOnlyTokenResult> {
432
+ const { address, accountIndex, expirySeconds, scope } = inputs
405
433
  const response = await this.fetcher({
406
434
  url: `${this.lighterApiUrl}/api/v1/tokens/create`,
407
435
  authorization,
@@ -410,6 +438,7 @@ export class LighterReadOnlyTokenManager {
410
438
  expiry: expirySeconds,
411
439
  subAccountAccess: scope === 'all',
412
440
  scopes: 'read.*',
441
+ fetchImpl: this.fetchImpl,
413
442
  })
414
443
 
415
444
  const token: LighterReadOnlyToken = {
@@ -457,6 +486,7 @@ export class LighterReadOnlyTokenManager {
457
486
  url: `${this.lighterApiUrl}/api/v1/tokens`,
458
487
  authorization,
459
488
  accountIndex,
489
+ fetchImpl: this.fetchImpl,
460
490
  })
461
491
  const nowSeconds = Math.floor(this.now() / 1000)
462
492
  const stale = rows
@@ -476,6 +506,7 @@ export class LighterReadOnlyTokenManager {
476
506
  authorization,
477
507
  tokenId: row.token_id,
478
508
  accountIndex,
509
+ fetchImpl: this.fetchImpl,
479
510
  })
480
511
  } catch (err) {
481
512
  console.warn(
@@ -508,6 +539,7 @@ export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
508
539
  expiry,
509
540
  subAccountAccess,
510
541
  scopes,
542
+ fetchImpl,
511
543
  }) => {
512
544
  const form = new FormData()
513
545
  form.set('name', name)
@@ -516,7 +548,7 @@ export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
516
548
  form.set('sub_account_access', String(subAccountAccess))
517
549
  form.set('scopes', scopes)
518
550
 
519
- const response = await fetch(url, {
551
+ const response = await (fetchImpl ?? fetch)(url, {
520
552
  method: 'POST',
521
553
  headers: { authorization },
522
554
  body: form,
@@ -544,10 +576,11 @@ export const defaultLighterTokenListFetcher: LighterTokenListFetcher = async ({
544
576
  url,
545
577
  authorization,
546
578
  accountIndex,
579
+ fetchImpl,
547
580
  }) => {
548
581
  const query = new URLSearchParams({ account_index: String(accountIndex) })
549
582
 
550
- const response = await fetch(`${url}?${query.toString()}`, {
583
+ const response = await (fetchImpl ?? fetch)(`${url}?${query.toString()}`, {
551
584
  headers: { authorization },
552
585
  })
553
586
 
@@ -570,13 +603,13 @@ export const defaultLighterTokenListFetcher: LighterTokenListFetcher = async ({
570
603
  * @internal
571
604
  */
572
605
  export const defaultLighterTokenRevokeFetcher: LighterTokenRevokeFetcher =
573
- async ({ url, authorization, tokenId, accountIndex }) => {
606
+ async ({ url, authorization, tokenId, accountIndex, fetchImpl }) => {
574
607
  const form = new URLSearchParams({
575
608
  token_id: String(tokenId),
576
609
  account_index: String(accountIndex),
577
610
  })
578
611
 
579
- const response = await fetch(url, {
612
+ const response = await (fetchImpl ?? fetch)(url, {
580
613
  method: 'POST',
581
614
  headers: {
582
615
  authorization,
@@ -304,11 +304,14 @@ async function signRegisterApiKey(
304
304
  l1Signature
305
305
  )
306
306
 
307
+ // The referral marker belongs to the L1 address, not to the key slot.
308
+ const previousKey = await deps.keyStore.get(address)
307
309
  await deps.keyStore.set(address, {
308
310
  accountIndex,
309
311
  apiKeyIndex,
310
312
  apiKeyPrivateKey: keypair.privateKey,
311
313
  apiKeyPublicKey: keypair.publicKey,
314
+ appliedReferralCode: previousKey?.appliedReferralCode,
312
315
  })
313
316
 
314
317
  return {
@@ -461,12 +464,15 @@ async function executeTokenAuthMutation(
461
464
  accountIndex: apiKey.accountIndex,
462
465
  })
463
466
 
467
+ let appliedReferralCode: string | undefined
464
468
  if (step.wasmSignParams.kind === 'referralUse') {
465
469
  const { referral_code } = step.wasmSignParams as { referral_code?: string }
470
+ appliedReferralCode = referral_code
466
471
  if (
467
472
  typeof referral_code === 'string' &&
468
473
  (await isReferralAlreadyApplied(deps, address, authToken, referral_code))
469
474
  ) {
475
+ await deps.keyStore.markReferralApplied(address, referral_code)
470
476
  return
471
477
  }
472
478
  }
@@ -485,6 +491,10 @@ async function executeTokenAuthMutation(
485
491
  `Lighter ${step.action} rejected (code ${code ?? status})${suffix}`
486
492
  )
487
493
  }
494
+
495
+ if (appliedReferralCode !== undefined) {
496
+ await deps.keyStore.markReferralApplied(address, appliedReferralCode)
497
+ }
488
498
  }
489
499
 
490
500
  /**
@@ -7,6 +7,7 @@ import { PerpsErrorCode } from '@lifi/perps-types'
7
7
  import {
8
8
  LIGHTER_INVALID_AUTH_CODE,
9
9
  LIGHTER_SUCCESS_CODES,
10
+ LIGHTER_TOKEN_REVOKED_CODE,
10
11
  } from '../constants.js'
11
12
 
12
13
  /** @internal */
@@ -26,19 +27,27 @@ const lighterBodyErrorCode = (data: unknown): number | undefined => {
26
27
  }
27
28
 
28
29
  /**
29
- * Auth-gated read whose token Lighter rejected. Distinct from a generic
30
- * {@link PerpsError} so callers can evict the stored read-only token and retry
31
- * with a freshly-created one. Lighter signals this on either channel: an HTTP
32
- * 401/403, or an HTTP 200 carrying an error `code` in the body.
30
+ * Auth-gated read whose token format Lighter rejected.
31
+ *
33
32
  * @internal
34
33
  */
35
34
  export class LighterAuthRejectedError extends PerpsError {}
36
35
 
36
+ /**
37
+ * Auth-gated read whose read-only token Lighter reports as revoked.
38
+ *
39
+ * @internal
40
+ */
41
+ export class LighterTokenRevokedError extends PerpsError {}
42
+
37
43
  const isLighterAuthRejection = (status: number, data: unknown): boolean =>
38
44
  status === 401 ||
39
45
  status === 403 ||
40
46
  lighterBodyErrorCode(data) === LIGHTER_INVALID_AUTH_CODE
41
47
 
48
+ const isLighterTokenRevoked = (data: unknown): boolean =>
49
+ lighterBodyErrorCode(data) === LIGHTER_TOKEN_REVOKED_CODE
50
+
42
51
  /** @internal */
43
52
  export const LIGHTER_RETRY_DEFAULTS: ResolvedRetryPolicy = {
44
53
  enabled: true,
@@ -47,9 +56,6 @@ export const LIGHTER_RETRY_DEFAULTS: ResolvedRetryPolicy = {
47
56
  maxDelayMs: 60_000,
48
57
  respectRetryAfter: true,
49
58
  classify: ({ response }) => {
50
- if (response.status === 429 || response.status === 405) {
51
- return 'retry-rate-limit'
52
- }
53
59
  if (
54
60
  response.status === 502 ||
55
61
  response.status === 503 ||
@@ -61,11 +67,42 @@ export const LIGHTER_RETRY_DEFAULTS: ResolvedRetryPolicy = {
61
67
  },
62
68
  }
63
69
 
70
+ const LIGHTER_RATE_LIMIT_HOLD_MS = 60_000
71
+ const LIGHTER_RATE_LIMIT_HOLD_MAX_MS = 300_000
72
+
73
+ const isLighterRateLimit = (status: number): boolean =>
74
+ status === 429 || status === 405
75
+
76
+ const retryAfterMs = (header: string | null, nowMs: number): number => {
77
+ if (header === null) {
78
+ return LIGHTER_RATE_LIMIT_HOLD_MS
79
+ }
80
+ const seconds = Number(header)
81
+ if (Number.isFinite(seconds) && seconds >= 0) {
82
+ return Math.min(seconds * 1000, LIGHTER_RATE_LIMIT_HOLD_MAX_MS)
83
+ }
84
+ const dateMs = Date.parse(header)
85
+ return Number.isFinite(dateMs)
86
+ ? Math.min(Math.max(0, dateMs - nowMs), LIGHTER_RATE_LIMIT_HOLD_MAX_MS)
87
+ : LIGHTER_RATE_LIMIT_HOLD_MS
88
+ }
89
+
90
+ /**
91
+ * Mutable rate-limit deadline shared by the request clients for one provider
92
+ * instance. A client creates an isolated hold when the caller omits this.
93
+ *
94
+ * @internal
95
+ */
96
+ export interface LighterRateLimitHold {
97
+ untilMs: number
98
+ }
99
+
64
100
  /** @internal */
65
101
  export interface LighterApiClientOptions {
66
102
  signal?: AbortSignal
67
103
  policy?: ResolvedRetryPolicy
68
104
  fetchImpl?: typeof fetch
105
+ rateLimitHold?: LighterRateLimitHold
69
106
  }
70
107
 
71
108
  /**
@@ -82,9 +119,10 @@ export interface LighterApiClientOptions {
82
119
  * `Authorization` header. This matches Lighter's OpenAPI spec and lets the
83
120
  * same call work browser-direct and from server-side proxies.
84
121
  *
85
- * Lighter signals rate limiting via 429 OR 405 (documented behaviour) with a
86
- * documented 60s firewall cooldown. The default {@link ResolvedRetryPolicy}
87
- * waits long enough to avoid hammering through the cooldown.
122
+ * Lighter signals rate limits through HTTP 429 or HTTP 405. This client never
123
+ * retries such a response. It throws `RateLimitExceeded` and holds all network
124
+ * dispatch until `Retry-After` expires (capped at five minutes), or for 60
125
+ * seconds when the response has no valid header.
88
126
  * @public
89
127
  */
90
128
  export class LighterApiClient {
@@ -92,14 +130,57 @@ export class LighterApiClient {
92
130
  private readonly signal: AbortSignal | undefined
93
131
  private readonly policy: ResolvedRetryPolicy
94
132
  private readonly fetchImpl: typeof fetch | undefined
133
+ private readonly rateLimitHold: LighterRateLimitHold
134
+ private readonly fetchWithHold: typeof fetch
95
135
 
96
136
  constructor(baseUrl: string, options?: LighterApiClientOptions) {
97
137
  this.baseUrl = baseUrl.replace(/\/$/, '')
98
138
  this.signal = options?.signal
99
139
  this.policy = options?.policy ?? LIGHTER_RETRY_DEFAULTS
100
140
  this.fetchImpl = options?.fetchImpl
141
+ this.rateLimitHold = options?.rateLimitHold ?? { untilMs: 0 }
142
+ this.fetchWithHold = async (input, init) => {
143
+ this.assertRequestAllowed()
144
+ const response = await (this.fetchImpl ?? fetch)(input, init)
145
+ if (isLighterRateLimit(response.status)) {
146
+ const responseNowMs = Date.now()
147
+ this.rateLimitHold.untilMs = Math.max(
148
+ this.rateLimitHold.untilMs,
149
+ responseNowMs +
150
+ retryAfterMs(response.headers.get('Retry-After'), responseNowMs)
151
+ )
152
+ }
153
+ return response
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Send a single request through this client's rate-limit hold.
159
+ *
160
+ * @internal
161
+ */
162
+ request(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
163
+ return this.fetchWithHold(input, init)
164
+ }
165
+
166
+ private rateLimitError(): PerpsError {
167
+ return new PerpsError(
168
+ PerpsErrorCode.RateLimitExceeded,
169
+ `Lighter API requests are held until ${new Date(this.rateLimitHold.untilMs).toISOString()}`
170
+ )
101
171
  }
102
172
 
173
+ private assertRequestAllowed(): void {
174
+ if (Date.now() < this.rateLimitHold.untilMs) {
175
+ throw this.rateLimitError()
176
+ }
177
+ }
178
+
179
+ private assertNotRateLimited(response: Response): void {
180
+ if (isLighterRateLimit(response.status)) {
181
+ throw this.rateLimitError()
182
+ }
183
+ }
103
184
  private buildUrl(path: string, params?: ApiParams): string {
104
185
  const url = `${this.baseUrl}${path}`
105
186
  if (!params || Object.keys(params).length === 0) {
@@ -131,6 +212,12 @@ export class LighterApiClient {
131
212
  ...params,
132
213
  auth: authToken,
133
214
  })
215
+ if (isLighterTokenRevoked(data)) {
216
+ throw new LighterTokenRevokedError(
217
+ PerpsErrorCode.ThirdPartyError,
218
+ `Lighter reports a revoked auth token for ${path}`
219
+ )
220
+ }
134
221
  if (isLighterAuthRejection(status, data)) {
135
222
  throw new LighterAuthRejectedError(
136
223
  PerpsErrorCode.ThirdPartyError,
@@ -150,12 +237,18 @@ export class LighterApiClient {
150
237
  path: string,
151
238
  params?: ApiParams
152
239
  ): Promise<{ status: number; data: T }> {
240
+ this.assertRequestAllowed()
153
241
  const url = this.buildUrl(path, params)
154
242
  const response = await fetchWithRetry(
155
243
  url,
156
244
  {},
157
- { policy: this.policy, fetchImpl: this.fetchImpl, signal: this.signal }
245
+ {
246
+ policy: this.policy,
247
+ fetchImpl: this.fetchWithHold,
248
+ signal: this.signal,
249
+ }
158
250
  )
251
+ this.assertNotRateLimited(response)
159
252
  const data = (await response.json().catch(() => undefined)) as T
160
253
  return { status: response.status, data }
161
254
  }
@@ -174,13 +267,13 @@ export class LighterApiClient {
174
267
  for (const [k, v] of Object.entries(params)) {
175
268
  body.set(k, String(v))
176
269
  }
177
- const fetchImpl = this.fetchImpl ?? fetch
178
- const response = await fetchImpl(`${this.baseUrl}${path}`, {
270
+ const response = await this.fetchWithHold(`${this.baseUrl}${path}`, {
179
271
  method: 'POST',
180
272
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
181
273
  body: body.toString(),
182
274
  signal: this.signal,
183
275
  })
276
+ this.assertNotRateLimited(response)
184
277
  const data = (await response.json().catch(() => undefined)) as T
185
278
  return { status: response.status, data }
186
279
  }