@lifi/perps-sdk-provider-lighter 21.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.
- package/dist/cjs/LighterProvider.d.ts.map +1 -1
- package/dist/cjs/LighterProvider.js +42 -20
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/constants.d.ts +1 -0
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +2 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/signers/signActions.d.ts.map +1 -1
- package/dist/cjs/signers/signActions.js +13 -5
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/types/order.d.ts +4 -0
- package/dist/cjs/types/order.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.d.ts +1 -1
- package/dist/cjs/utils/apiClient.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.js +15 -7
- package/dist/cjs/utils/apiClient.js.map +1 -1
- package/dist/cjs/utils/lighterErrorCode.d.ts +3 -0
- package/dist/cjs/utils/lighterErrorCode.d.ts.map +1 -0
- package/dist/cjs/utils/lighterErrorCode.js +17 -0
- package/dist/cjs/utils/lighterErrorCode.js.map +1 -0
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +63 -28
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/constants.d.ts +14 -2
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +14 -2
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/signers/signActions.d.ts.map +1 -1
- package/dist/esm/signers/signActions.js +22 -8
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/types/order.d.ts +12 -0
- package/dist/esm/types/order.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.d.ts +15 -7
- package/dist/esm/utils/apiClient.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.js +32 -14
- package/dist/esm/utils/apiClient.js.map +1 -1
- package/dist/esm/utils/lighterErrorCode.d.ts +7 -0
- package/dist/esm/utils/lighterErrorCode.d.ts.map +1 -0
- package/dist/esm/utils/lighterErrorCode.js +26 -0
- package/dist/esm/utils/lighterErrorCode.js.map +1 -0
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/constants.d.ts +14 -2
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/types/order.d.ts +12 -0
- package/dist/types/types/order.d.ts.map +1 -1
- package/dist/types/utils/apiClient.d.ts +15 -7
- package/dist/types/utils/apiClient.d.ts.map +1 -1
- package/dist/types/utils/lighterErrorCode.d.ts +7 -0
- package/dist/types/utils/lighterErrorCode.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/LighterProvider.ts +104 -50
- package/src/constants.ts +15 -2
- package/src/index.ts +1 -0
- package/src/signers/signActions.ts +28 -11
- package/src/types/order.ts +13 -0
- package/src/utils/apiClient.ts +36 -13
- package/src/utils/lighterErrorCode.ts +30 -0
package/src/LighterProvider.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getAssetRegistry,
|
|
5
5
|
getMarketRegistry,
|
|
6
6
|
getProviders,
|
|
7
|
+
isActiveOrderStatus,
|
|
7
8
|
localStorageAdapter,
|
|
8
9
|
PerpsError,
|
|
9
10
|
type PerpsProviderPlugin,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
DEFAULT_TRADES_LIMIT,
|
|
67
68
|
LIGHTER_ALL_MARKETS_WILDCARD,
|
|
68
69
|
LIGHTER_BASE_FEE_TIER,
|
|
70
|
+
LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX,
|
|
69
71
|
LIGHTER_FEE_TICK_SCALE,
|
|
70
72
|
LIGHTER_HISTORY_PAGE_SIZE,
|
|
71
73
|
LIGHTER_MAINNET_DEPLOYMENT,
|
|
@@ -88,6 +90,7 @@ import {
|
|
|
88
90
|
import type {
|
|
89
91
|
LtAccount,
|
|
90
92
|
LtAccountLimits,
|
|
93
|
+
LtAccountOrdersResponse,
|
|
91
94
|
LtDepositHistoryItem,
|
|
92
95
|
LtDepositHistoryResponse,
|
|
93
96
|
LtDetailedAccountPosition,
|
|
@@ -165,6 +168,13 @@ const orderCountFor = (p: LtDetailedAccountPosition): number =>
|
|
|
165
168
|
*/
|
|
166
169
|
const TX_HASH_PATTERN = /^[0-9a-f]{80}$/
|
|
167
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Lighter's `client_order_index` is an int64 and `accountOrders` takes it as a
|
|
173
|
+
* decimal string, so a prefixed id's suffix goes to the venue verbatim rather
|
|
174
|
+
* than through a lossy JS number.
|
|
175
|
+
*/
|
|
176
|
+
const CLIENT_ORDER_INDEX_PATTERN = /^\d+$/
|
|
177
|
+
|
|
168
178
|
const INACTIVE_ORDERS_LOOKUP_LIMIT = 100
|
|
169
179
|
|
|
170
180
|
/** Activity surfaces whose rows name a market, so they need the market registry. */
|
|
@@ -567,7 +577,7 @@ export const createLighterProvider = (
|
|
|
567
577
|
account_index: accountIndex,
|
|
568
578
|
})
|
|
569
579
|
|
|
570
|
-
const
|
|
580
|
+
const fetchActiveOrders = async (
|
|
571
581
|
client: LighterApiClient,
|
|
572
582
|
authToken: string,
|
|
573
583
|
accountIndex: number,
|
|
@@ -581,6 +591,20 @@ export const createLighterProvider = (
|
|
|
581
591
|
return { ...response, orders: wireList(response.orders) }
|
|
582
592
|
}
|
|
583
593
|
|
|
594
|
+
const fetchOrdersByClientOrderIndex = async (
|
|
595
|
+
client: LighterApiClient,
|
|
596
|
+
authToken: string,
|
|
597
|
+
accountIndex: number,
|
|
598
|
+
clientOrderIndex: string
|
|
599
|
+
): Promise<LtOrder[]> => {
|
|
600
|
+
const response = await client.getAuthed<LtAccountOrdersResponse>(
|
|
601
|
+
'/api/v1/accountOrders',
|
|
602
|
+
authToken,
|
|
603
|
+
{ account_index: accountIndex, client_order_indexes: clientOrderIndex }
|
|
604
|
+
)
|
|
605
|
+
return wireList(response.orders)
|
|
606
|
+
}
|
|
607
|
+
|
|
584
608
|
const deriveOrderBearingMarketIds = (account: LtAccount): number[] =>
|
|
585
609
|
account.positions
|
|
586
610
|
.filter((p) => orderCountFor(p) > 0)
|
|
@@ -1017,21 +1041,17 @@ export const createLighterProvider = (
|
|
|
1017
1041
|
registry.sync(),
|
|
1018
1042
|
])
|
|
1019
1043
|
|
|
1020
|
-
const
|
|
1044
|
+
const marketId =
|
|
1021
1045
|
params.marketId === undefined
|
|
1022
|
-
?
|
|
1023
|
-
:
|
|
1046
|
+
? LIGHTER_ALL_MARKETS_WILDCARD
|
|
1047
|
+
: Number(registry.require(params.marketId).id)
|
|
1024
1048
|
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
marketIds.map((id) =>
|
|
1028
|
-
fetchActiveOrdersForMarket(client, t, account.index, id)
|
|
1029
|
-
)
|
|
1030
|
-
)
|
|
1049
|
+
const response = await retryOnRevoked(opts, params.address, token, (t) =>
|
|
1050
|
+
fetchActiveOrders(client, t, account.index, marketId)
|
|
1031
1051
|
)
|
|
1032
1052
|
|
|
1033
1053
|
const { openOrders, triggerOrders } = classifyAndMapOrders(
|
|
1034
|
-
|
|
1054
|
+
response.orders,
|
|
1035
1055
|
(marketIndex) => registry.require(String(marketIndex))
|
|
1036
1056
|
)
|
|
1037
1057
|
|
|
@@ -1065,9 +1085,7 @@ export const createLighterProvider = (
|
|
|
1065
1085
|
: [Number(registry.require(params.marketId).id)]
|
|
1066
1086
|
const responses = await retryOnRevoked(opts, params.address, token, (t) =>
|
|
1067
1087
|
Promise.all(
|
|
1068
|
-
marketIds.map((id) =>
|
|
1069
|
-
fetchActiveOrdersForMarket(client, t, account.index, id)
|
|
1070
|
-
)
|
|
1088
|
+
marketIds.map((id) => fetchActiveOrders(client, t, account.index, id))
|
|
1071
1089
|
)
|
|
1072
1090
|
)
|
|
1073
1091
|
|
|
@@ -1087,6 +1105,14 @@ export const createLighterProvider = (
|
|
|
1087
1105
|
return twaps
|
|
1088
1106
|
},
|
|
1089
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolves either reference Lighter hands out for an order: a bare
|
|
1110
|
+
* `order_index` — the value `Order.orderId` carries, as streamed by
|
|
1111
|
+
* `orderUpdates` / `fills` — or a `client_order_index` prefixed with
|
|
1112
|
+
* {@link LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}, which a signed placement
|
|
1113
|
+
* step returns and which resolves before the venue has assigned an
|
|
1114
|
+
* `order_index`.
|
|
1115
|
+
*/
|
|
1090
1116
|
async getOrder(
|
|
1091
1117
|
params: ProviderGetOrderParams,
|
|
1092
1118
|
opts?: SDKRequestOptions
|
|
@@ -1108,41 +1134,75 @@ export const createLighterProvider = (
|
|
|
1108
1134
|
registry.sync(),
|
|
1109
1135
|
])
|
|
1110
1136
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1137
|
+
const notFound = (): PerpsError =>
|
|
1138
|
+
new PerpsError(
|
|
1139
|
+
PerpsErrorCode.OrderNotFound,
|
|
1140
|
+
`Lighter order ${params.id} not found for ${params.address}`
|
|
1141
|
+
)
|
|
1142
|
+
const detail = (order: LtOrder): Order =>
|
|
1143
|
+
mapOrderDetail(order, registry.require(String(order.market_index)))
|
|
1144
|
+
|
|
1145
|
+
// A tx-hash route would require mapping the caller's executeAction tx
|
|
1146
|
+
// hash → wasm nonce → matching order, which the LI.FI backend did via
|
|
1147
|
+
// its `UserAction` table; the SDK has no equivalent persistence, so we
|
|
1148
|
+
// refuse rather than mis-resolve.
|
|
1117
1149
|
if (TX_HASH_PATTERN.test(params.id)) {
|
|
1118
1150
|
throw new PerpsError(
|
|
1119
1151
|
PerpsErrorCode.OrderNotFound,
|
|
1120
1152
|
`Lighter order id "${params.id}" looks like a tx hash. The SDK ` +
|
|
1121
|
-
`resolves orders by Lighter \`order_index\`
|
|
1122
|
-
|
|
1153
|
+
`resolves orders by Lighter \`order_index\` or by a ` +
|
|
1154
|
+
`\`${LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}<index>\` id — surface the ` +
|
|
1155
|
+
`orderId from the orderUpdates / fills WS stream, or the ` +
|
|
1156
|
+
`clientOrderIndex from the signed placement step, and pass it here.`
|
|
1123
1157
|
)
|
|
1124
1158
|
}
|
|
1125
|
-
const predicate: (o: { order_index: number }) => boolean = (o) =>
|
|
1126
|
-
String(o.order_index) === params.id
|
|
1127
1159
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
)
|
|
1160
|
+
if (params.id.startsWith(LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX)) {
|
|
1161
|
+
const clientOrderIndex = params.id.slice(
|
|
1162
|
+
LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX.length
|
|
1163
|
+
)
|
|
1164
|
+
if (!CLIENT_ORDER_INDEX_PATTERN.test(clientOrderIndex)) {
|
|
1165
|
+
throw new PerpsError(
|
|
1166
|
+
PerpsErrorCode.SDKError,
|
|
1167
|
+
`Lighter order id "${params.id}" names a client order index, but ` +
|
|
1168
|
+
`"${clientOrderIndex}" is not a decimal integer.`
|
|
1138
1169
|
)
|
|
1139
|
-
)
|
|
1140
|
-
|
|
1141
|
-
for (const response of activeResponses) {
|
|
1142
|
-
const hit = response.orders.find(predicate as (o: unknown) => boolean)
|
|
1143
|
-
if (hit !== undefined) {
|
|
1144
|
-
return mapOrderDetail(hit, registry.require(String(hit.market_index)))
|
|
1145
1170
|
}
|
|
1171
|
+
const orders = await retryOnRevoked(opts, params.address, token, (t) =>
|
|
1172
|
+
fetchOrdersByClientOrderIndex(
|
|
1173
|
+
client,
|
|
1174
|
+
t,
|
|
1175
|
+
account.index,
|
|
1176
|
+
clientOrderIndex
|
|
1177
|
+
)
|
|
1178
|
+
)
|
|
1179
|
+
// `accountOrders` reaches active and inactive rows alike, so a reused
|
|
1180
|
+
// client order index can match more than one row. Prefer the live one.
|
|
1181
|
+
const matches = orders
|
|
1182
|
+
.filter((o) => String(o.client_order_index) === clientOrderIndex)
|
|
1183
|
+
.map(detail)
|
|
1184
|
+
const hit =
|
|
1185
|
+
matches.find((o) => isActiveOrderStatus(o.status)) ?? matches[0]
|
|
1186
|
+
if (hit === undefined) {
|
|
1187
|
+
throw notFound()
|
|
1188
|
+
}
|
|
1189
|
+
return hit
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const byOrderIndex = (o: LtOrder): boolean =>
|
|
1193
|
+
String(o.order_index) === params.id
|
|
1194
|
+
|
|
1195
|
+
const active = await retryOnRevoked(opts, params.address, token, (t) =>
|
|
1196
|
+
fetchActiveOrders(
|
|
1197
|
+
client,
|
|
1198
|
+
t,
|
|
1199
|
+
account.index,
|
|
1200
|
+
LIGHTER_ALL_MARKETS_WILDCARD
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1203
|
+
const activeHit = active.orders.find(byOrderIndex)
|
|
1204
|
+
if (activeHit !== undefined) {
|
|
1205
|
+
return detail(activeHit)
|
|
1146
1206
|
}
|
|
1147
1207
|
|
|
1148
1208
|
const inactive = await retryOnRevoked(opts, params.address, token, (t) =>
|
|
@@ -1152,17 +1212,11 @@ export const createLighterProvider = (
|
|
|
1152
1212
|
limit: INACTIVE_ORDERS_LOOKUP_LIMIT,
|
|
1153
1213
|
})
|
|
1154
1214
|
)
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (hit !== undefined) {
|
|
1159
|
-
return mapOrderDetail(hit, registry.require(String(hit.market_index)))
|
|
1215
|
+
const inactiveHit = wireList(inactive.orders).find(byOrderIndex)
|
|
1216
|
+
if (inactiveHit === undefined) {
|
|
1217
|
+
throw notFound()
|
|
1160
1218
|
}
|
|
1161
|
-
|
|
1162
|
-
throw new PerpsError(
|
|
1163
|
-
PerpsErrorCode.OrderNotFound,
|
|
1164
|
-
`Lighter order ${params.id} not found for ${params.address}`
|
|
1165
|
-
)
|
|
1219
|
+
return detail(inactiveHit)
|
|
1166
1220
|
},
|
|
1167
1221
|
|
|
1168
1222
|
async getFills(
|
package/src/constants.ts
CHANGED
|
@@ -204,13 +204,26 @@ export const LIGHTER_BASE_FEE_TIER: FeeTier = { maker: '0', taker: '0' }
|
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* Wildcard `market_id` accepted by Lighter's per-market account endpoints to
|
|
207
|
-
* indicate "every market". Used on `
|
|
208
|
-
* `liquidations`.
|
|
207
|
+
* indicate "every market". Used on `accountActiveOrders`,
|
|
208
|
+
* `accountInactiveOrders` and `liquidations`. It is also the venue's own
|
|
209
|
+
* documented default for `accountActiveOrders.market_id`.
|
|
209
210
|
*
|
|
210
211
|
* @internal
|
|
211
212
|
*/
|
|
212
213
|
export const LIGHTER_ALL_MARKETS_WILDCARD = 255
|
|
213
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Prefix that marks a `getOrder` id as a Lighter `client_order_index` rather
|
|
217
|
+
* than an `order_index`. Compose the id as
|
|
218
|
+
* `` `${LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}${clientOrderIndex}` `` from the
|
|
219
|
+
* `clientOrderIndex` a signed placement step returns. Both references are
|
|
220
|
+
* decimal integers, so without the prefix `getOrder` could resolve one as the
|
|
221
|
+
* other and return an unrelated order.
|
|
222
|
+
*
|
|
223
|
+
* @public
|
|
224
|
+
*/
|
|
225
|
+
export const LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX = 'client_order_index:'
|
|
226
|
+
|
|
214
227
|
/**
|
|
215
228
|
* Lighter caps `limit` at 100 on positionFunding/liquidations endpoints.
|
|
216
229
|
*
|
package/src/index.ts
CHANGED
|
@@ -230,13 +230,30 @@ async function signStandardWasmAction(
|
|
|
230
230
|
apiKeyIndex: apiKey.apiKeyIndex,
|
|
231
231
|
accountIndex: apiKey.accountIndex,
|
|
232
232
|
})
|
|
233
|
+
const clientOrderIndex = clientOrderIndexOf(step.wasmSignParams)
|
|
233
234
|
return {
|
|
234
235
|
action: step.action,
|
|
235
236
|
wasmSignParams: step.wasmSignParams,
|
|
236
237
|
signedTx,
|
|
238
|
+
...(clientOrderIndex === undefined ? {} : { clientOrderIndex }),
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Project the signed order's `client_order_index` so the caller can resolve it
|
|
244
|
+
* through `getOrder` before the venue assigns an `order_index`. Only the
|
|
245
|
+
* order-placing actions carry one, and the value is read back off the params
|
|
246
|
+
* that were signed rather than generated a second time.
|
|
247
|
+
*/
|
|
248
|
+
function clientOrderIndexOf(
|
|
249
|
+
wasmSignParams: Record<string, unknown>
|
|
250
|
+
): string | undefined {
|
|
251
|
+
const value = wasmSignParams.client_order_index
|
|
252
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
253
|
+
? String(value)
|
|
254
|
+
: undefined
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
/**
|
|
241
258
|
* REGISTER_API_KEY flow:
|
|
242
259
|
* 1. Look up the user's Lighter accountIndex (Lighter REST).
|
|
@@ -484,11 +501,11 @@ async function executeTokenAuthMutation(
|
|
|
484
501
|
}
|
|
485
502
|
}
|
|
486
503
|
|
|
487
|
-
const { path, params } = buildTokenAuthMutationRequest(step
|
|
504
|
+
const { path, params } = buildTokenAuthMutationRequest(step)
|
|
488
505
|
const { status, data } = await deps.apiClient.postForm<{
|
|
489
506
|
code?: number
|
|
490
507
|
message?: string
|
|
491
|
-
}>(path, params)
|
|
508
|
+
}>(path, authToken, params)
|
|
492
509
|
|
|
493
510
|
const code = data?.code
|
|
494
511
|
const referralAlreadyApplied =
|
|
@@ -533,14 +550,14 @@ async function readAppliedReferralCode(
|
|
|
533
550
|
}
|
|
534
551
|
|
|
535
552
|
/**
|
|
536
|
-
* Map a token-authenticated mutation step to its Lighter endpoint and
|
|
537
|
-
*
|
|
538
|
-
* `
|
|
553
|
+
* Map a token-authenticated mutation step to its Lighter endpoint and form
|
|
554
|
+
* body. The fields come from `wasmSignParams`; the auth token travels in the
|
|
555
|
+
* `Authorization` header, not the body.
|
|
539
556
|
*/
|
|
540
|
-
function buildTokenAuthMutationRequest(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
557
|
+
function buildTokenAuthMutationRequest(step: WasmBlobActionStep): {
|
|
558
|
+
path: string
|
|
559
|
+
params: ApiParams
|
|
560
|
+
} {
|
|
544
561
|
const kind = step.wasmSignParams.kind
|
|
545
562
|
if (kind === 'changeAccountTier') {
|
|
546
563
|
const { account_index, new_tier } = step.wasmSignParams as {
|
|
@@ -555,7 +572,7 @@ function buildTokenAuthMutationRequest(
|
|
|
555
572
|
}
|
|
556
573
|
return {
|
|
557
574
|
path: '/api/v1/changeAccountTier',
|
|
558
|
-
params: {
|
|
575
|
+
params: { account_index, new_tier },
|
|
559
576
|
}
|
|
560
577
|
}
|
|
561
578
|
if (kind === 'referralUse') {
|
|
@@ -572,7 +589,7 @@ function buildTokenAuthMutationRequest(
|
|
|
572
589
|
}
|
|
573
590
|
return {
|
|
574
591
|
path: '/api/v1/referral/use',
|
|
575
|
-
params: {
|
|
592
|
+
params: { l1_address, referral_code, x },
|
|
576
593
|
}
|
|
577
594
|
}
|
|
578
595
|
throw new PerpsError(
|
package/src/types/order.ts
CHANGED
|
@@ -69,6 +69,19 @@ export interface LtOrdersResponse {
|
|
|
69
69
|
orders: LtOrder[] | null
|
|
70
70
|
}
|
|
71
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
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
/**
|
|
73
86
|
* Single order-book level returned by Lighter. Amounts and prices are decimal
|
|
74
87
|
* strings in the market's native precision; `order_expiry` is an absolute
|
package/src/utils/apiClient.ts
CHANGED
|
@@ -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>
|
|
@@ -50,9 +51,17 @@ const isLighterTokenRevoked = (data: unknown): boolean =>
|
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* A CR or LF in a token would split the request when it reaches a
|
|
53
|
-
* `fetchImpl` that does not validate header values itself.
|
|
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.
|
|
54
57
|
*/
|
|
55
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
|
+
}
|
|
56
65
|
if (/[\r\n]/.test(authToken)) {
|
|
57
66
|
throw new PerpsError(
|
|
58
67
|
PerpsErrorCode.ValidationError,
|
|
@@ -136,6 +145,11 @@ export interface LighterApiClientOptions {
|
|
|
136
145
|
* `Access-Control-Max-Age: 86400`, so a browser sends one preflight per day
|
|
137
146
|
* per origin.
|
|
138
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.
|
|
152
|
+
*
|
|
139
153
|
* Lighter signals rate limits through HTTP 429 or HTTP 405. This client never
|
|
140
154
|
* retries such a response. It throws `RateLimitExceeded` and holds all network
|
|
141
155
|
* dispatch until `Retry-After` expires (capped at five minutes), or for 60
|
|
@@ -226,13 +240,13 @@ export class LighterApiClient {
|
|
|
226
240
|
})
|
|
227
241
|
if (isLighterTokenRevoked(data)) {
|
|
228
242
|
throw new LighterTokenRevokedError(
|
|
229
|
-
PerpsErrorCode.
|
|
243
|
+
PerpsErrorCode.Unauthorized,
|
|
230
244
|
`Lighter reports a revoked auth token for ${path}`
|
|
231
245
|
)
|
|
232
246
|
}
|
|
233
247
|
if (isLighterAuthRejection(status, data)) {
|
|
234
248
|
throw new LighterAuthRejectedError(
|
|
235
|
-
PerpsErrorCode.
|
|
249
|
+
PerpsErrorCode.Unauthorized,
|
|
236
250
|
`Lighter rejected the auth token for ${path}`
|
|
237
251
|
)
|
|
238
252
|
}
|
|
@@ -274,13 +288,15 @@ export class LighterApiClient {
|
|
|
274
288
|
}
|
|
275
289
|
|
|
276
290
|
/**
|
|
277
|
-
* Form-encoded POST to a Lighter mutation endpoint
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
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.
|
|
281
296
|
*/
|
|
282
297
|
async postForm<T>(
|
|
283
298
|
path: string,
|
|
299
|
+
authToken: string,
|
|
284
300
|
params: ApiParams
|
|
285
301
|
): Promise<{ status: number; data: T }> {
|
|
286
302
|
const body = new URLSearchParams()
|
|
@@ -289,7 +305,10 @@ export class LighterApiClient {
|
|
|
289
305
|
}
|
|
290
306
|
const response = await this.fetchWithHold(`${this.baseUrl}${path}`, {
|
|
291
307
|
method: 'POST',
|
|
292
|
-
headers: {
|
|
308
|
+
headers: {
|
|
309
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
310
|
+
Authorization: assertHeaderSafe(authToken),
|
|
311
|
+
},
|
|
293
312
|
body: body.toString(),
|
|
294
313
|
signal: this.signal,
|
|
295
314
|
})
|
|
@@ -306,20 +325,24 @@ export class LighterApiClient {
|
|
|
306
325
|
|
|
307
326
|
/**
|
|
308
327
|
* Post-parse validation shared by every checked read: rejects a non-2xx HTTP
|
|
309
|
-
* status and a 200 body carrying a non-success `code`.
|
|
310
|
-
*
|
|
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.
|
|
311
332
|
*/
|
|
312
333
|
private assertOk(path: string, status: number, data: unknown): void {
|
|
334
|
+
const errorCode = lighterBodyErrorCode(data)
|
|
335
|
+
const code =
|
|
336
|
+
lighterErrorCodeFromBody(errorCode) ?? PerpsErrorCode.ThirdPartyError
|
|
313
337
|
if (status < 200 || status >= 300) {
|
|
314
338
|
throw new PerpsError(
|
|
315
|
-
|
|
339
|
+
code,
|
|
316
340
|
`Lighter API request failed: ${status} — ${JSON.stringify(data).slice(0, 200)}`
|
|
317
341
|
)
|
|
318
342
|
}
|
|
319
|
-
const errorCode = lighterBodyErrorCode(data)
|
|
320
343
|
if (errorCode !== undefined) {
|
|
321
344
|
throw new PerpsError(
|
|
322
|
-
|
|
345
|
+
code,
|
|
323
346
|
`Lighter API error for ${path}: code ${errorCode} — ${JSON.stringify(data).slice(0, 200)}`
|
|
324
347
|
)
|
|
325
348
|
}
|
|
@@ -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)
|