@lifi/perps-sdk-provider-lighter 17.3.0 → 18.0.1
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.
- package/README.md +8 -0
- package/dist/cjs/LighterProvider.d.ts.map +1 -1
- package/dist/cjs/LighterProvider.js +76 -57
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/accountConfig.d.ts.map +1 -1
- package/dist/cjs/accountConfig.js.map +1 -1
- package/dist/cjs/constants.d.ts +2 -1
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +3 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts +3 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.js +14 -2
- package/dist/cjs/signers/LighterKeyStore.js.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts +7 -0
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.js +19 -7
- package/dist/cjs/signers/LighterReadOnlyTokenManager.js.map +1 -1
- package/dist/cjs/signers/signActions.d.ts.map +1 -1
- package/dist/cjs/signers/signActions.js +8 -0
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/types/ws.d.ts +3 -3
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.d.ts +12 -0
- package/dist/cjs/utils/apiClient.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.js +62 -7
- package/dist/cjs/utils/apiClient.js.map +1 -1
- package/dist/cjs/utils/mapMarketContext.js +2 -2
- package/dist/cjs/utils/mapMarketContext.js.map +1 -1
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +78 -102
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/accountConfig.d.ts.map +1 -1
- package/dist/esm/accountConfig.js +2 -4
- package/dist/esm/accountConfig.js.map +1 -1
- package/dist/esm/constants.d.ts +7 -1
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +7 -1
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/signers/LighterKeyStore.d.ts +4 -1
- package/dist/esm/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/esm/signers/LighterKeyStore.js +15 -3
- package/dist/esm/signers/LighterKeyStore.js.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts +12 -0
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.js +23 -7
- package/dist/esm/signers/LighterReadOnlyTokenManager.js.map +1 -1
- package/dist/esm/signers/signActions.d.ts.map +1 -1
- package/dist/esm/signers/signActions.js +9 -0
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/types/ws.d.ts +6 -5
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.d.ts +34 -7
- package/dist/esm/utils/apiClient.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.js +77 -14
- package/dist/esm/utils/apiClient.js.map +1 -1
- package/dist/esm/utils/mapMarketContext.js +2 -2
- package/dist/esm/utils/mapMarketContext.js.map +1 -1
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/accountConfig.d.ts.map +1 -1
- package/dist/types/constants.d.ts +7 -1
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/signers/LighterKeyStore.d.ts +4 -1
- package/dist/types/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts +12 -0
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/types/ws.d.ts +6 -5
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/utils/apiClient.d.ts +34 -7
- package/dist/types/utils/apiClient.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/LighterProvider.ts +103 -138
- package/src/accountConfig.ts +2 -4
- package/src/constants.ts +8 -0
- package/src/signers/LighterKeyStore.ts +28 -3
- package/src/signers/LighterReadOnlyTokenManager.ts +39 -6
- package/src/signers/signActions.ts +10 -0
- package/src/types/ws.ts +6 -5
- package/src/utils/apiClient.ts +106 -13
- package/src/utils/mapMarketContext.ts +2 -2
package/src/utils/apiClient.ts
CHANGED
|
@@ -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.
|
|
30
|
-
*
|
|
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
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
-
{
|
|
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
|
|
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
|
}
|
|
@@ -22,8 +22,8 @@ export const mapMarketContext = (
|
|
|
22
22
|
midPrice: stats.mid_price,
|
|
23
23
|
markPrice: stats.mark_price,
|
|
24
24
|
oraclePrice: stats.index_price,
|
|
25
|
-
priceChange24h: stats.daily_price_change,
|
|
26
|
-
volume24h: stats.daily_quote_token_volume,
|
|
25
|
+
priceChange24h: String(stats.daily_price_change),
|
|
26
|
+
volume24h: String(stats.daily_quote_token_volume),
|
|
27
27
|
openInterest: stats.open_interest,
|
|
28
28
|
funding: {
|
|
29
29
|
rate: stats.current_funding_rate,
|