@lifi/perps-sdk-provider-lighter 21.0.0 → 22.1.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 (111) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +53 -26
  3. package/dist/cjs/LighterProvider.js.map +1 -1
  4. package/dist/cjs/accountConfig.js +3 -4
  5. package/dist/cjs/accountConfig.js.map +1 -1
  6. package/dist/cjs/constants.d.ts +3 -1
  7. package/dist/cjs/constants.d.ts.map +1 -1
  8. package/dist/cjs/constants.js +4 -1
  9. package/dist/cjs/constants.js.map +1 -1
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +2 -1
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  15. package/dist/cjs/signers/signActions.js +13 -5
  16. package/dist/cjs/signers/signActions.js.map +1 -1
  17. package/dist/cjs/types/account.d.ts +3 -3
  18. package/dist/cjs/types/account.d.ts.map +1 -1
  19. package/dist/cjs/types/action.d.ts +2 -0
  20. package/dist/cjs/types/action.d.ts.map +1 -1
  21. package/dist/cjs/types/action.js +3 -1
  22. package/dist/cjs/types/action.js.map +1 -1
  23. package/dist/cjs/types/fill.d.ts +2 -2
  24. package/dist/cjs/types/fill.d.ts.map +1 -1
  25. package/dist/cjs/types/order.d.ts +4 -0
  26. package/dist/cjs/types/order.d.ts.map +1 -1
  27. package/dist/cjs/utils/apiClient.d.ts +1 -1
  28. package/dist/cjs/utils/apiClient.d.ts.map +1 -1
  29. package/dist/cjs/utils/apiClient.js +23 -10
  30. package/dist/cjs/utils/apiClient.js.map +1 -1
  31. package/dist/cjs/utils/lighterErrorCode.d.ts +3 -0
  32. package/dist/cjs/utils/lighterErrorCode.d.ts.map +1 -0
  33. package/dist/cjs/utils/lighterErrorCode.js +17 -0
  34. package/dist/cjs/utils/lighterErrorCode.js.map +1 -0
  35. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  36. package/dist/cjs/utils/mapFill.js +2 -2
  37. package/dist/cjs/utils/mapFill.js.map +1 -1
  38. package/dist/cjs/utils/mapPosition.js +1 -1
  39. package/dist/cjs/utils/mapPosition.js.map +1 -1
  40. package/dist/esm/LighterProvider.d.ts.map +1 -1
  41. package/dist/esm/LighterProvider.js +79 -35
  42. package/dist/esm/LighterProvider.js.map +1 -1
  43. package/dist/esm/accountConfig.js +5 -7
  44. package/dist/esm/accountConfig.js.map +1 -1
  45. package/dist/esm/constants.d.ts +24 -3
  46. package/dist/esm/constants.d.ts.map +1 -1
  47. package/dist/esm/constants.js +24 -3
  48. package/dist/esm/constants.js.map +1 -1
  49. package/dist/esm/index.d.ts +1 -1
  50. package/dist/esm/index.d.ts.map +1 -1
  51. package/dist/esm/index.js +1 -1
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/signers/signActions.d.ts.map +1 -1
  54. package/dist/esm/signers/signActions.js +22 -8
  55. package/dist/esm/signers/signActions.js.map +1 -1
  56. package/dist/esm/types/account.d.ts +15 -4
  57. package/dist/esm/types/account.d.ts.map +1 -1
  58. package/dist/esm/types/action.d.ts +13 -0
  59. package/dist/esm/types/action.d.ts.map +1 -1
  60. package/dist/esm/types/action.js +13 -0
  61. package/dist/esm/types/action.js.map +1 -1
  62. package/dist/esm/types/fill.d.ts +2 -2
  63. package/dist/esm/types/fill.d.ts.map +1 -1
  64. package/dist/esm/types/order.d.ts +12 -0
  65. package/dist/esm/types/order.d.ts.map +1 -1
  66. package/dist/esm/utils/apiClient.d.ts +15 -7
  67. package/dist/esm/utils/apiClient.d.ts.map +1 -1
  68. package/dist/esm/utils/apiClient.js +41 -18
  69. package/dist/esm/utils/apiClient.js.map +1 -1
  70. package/dist/esm/utils/lighterErrorCode.d.ts +7 -0
  71. package/dist/esm/utils/lighterErrorCode.d.ts.map +1 -0
  72. package/dist/esm/utils/lighterErrorCode.js +26 -0
  73. package/dist/esm/utils/lighterErrorCode.js.map +1 -0
  74. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  75. package/dist/esm/utils/mapFill.js +3 -2
  76. package/dist/esm/utils/mapFill.js.map +1 -1
  77. package/dist/esm/utils/mapPosition.js +1 -1
  78. package/dist/esm/utils/mapPosition.js.map +1 -1
  79. package/dist/types/LighterProvider.d.ts.map +1 -1
  80. package/dist/types/constants.d.ts +24 -3
  81. package/dist/types/constants.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +1 -1
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/signers/signActions.d.ts.map +1 -1
  85. package/dist/types/types/account.d.ts +15 -4
  86. package/dist/types/types/account.d.ts.map +1 -1
  87. package/dist/types/types/action.d.ts +13 -0
  88. package/dist/types/types/action.d.ts.map +1 -1
  89. package/dist/types/types/fill.d.ts +2 -2
  90. package/dist/types/types/fill.d.ts.map +1 -1
  91. package/dist/types/types/order.d.ts +12 -0
  92. package/dist/types/types/order.d.ts.map +1 -1
  93. package/dist/types/utils/apiClient.d.ts +15 -7
  94. package/dist/types/utils/apiClient.d.ts.map +1 -1
  95. package/dist/types/utils/lighterErrorCode.d.ts +7 -0
  96. package/dist/types/utils/lighterErrorCode.d.ts.map +1 -0
  97. package/dist/types/utils/mapFill.d.ts.map +1 -1
  98. package/package.json +2 -2
  99. package/src/LighterProvider.ts +130 -57
  100. package/src/accountConfig.ts +8 -8
  101. package/src/constants.ts +26 -2
  102. package/src/index.ts +1 -0
  103. package/src/signers/signActions.ts +28 -11
  104. package/src/types/account.ts +15 -4
  105. package/src/types/action.ts +15 -0
  106. package/src/types/fill.ts +3 -2
  107. package/src/types/order.ts +13 -0
  108. package/src/utils/apiClient.ts +47 -17
  109. package/src/utils/lighterErrorCode.ts +30 -0
  110. package/src/utils/mapFill.ts +5 -3
  111. package/src/utils/mapPosition.ts +1 -1
@@ -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,
@@ -103,7 +106,11 @@ import type {
103
106
  LtWithdrawHistoryItem,
104
107
  LtWithdrawHistoryResponse,
105
108
  } from './types/index.js'
106
- import { LT_MARGIN_MODE_CROSS, LT_MARGIN_MODE_ISOLATED } from './types/index.js'
109
+ import {
110
+ LT_ACCOUNT_TRADING_MODE_SIMPLE,
111
+ LT_MARGIN_MODE_CROSS,
112
+ LT_MARGIN_MODE_ISOLATED,
113
+ } from './types/index.js'
107
114
  import {
108
115
  decodeActivityCursor,
109
116
  encodeActivityCursor,
@@ -165,6 +172,13 @@ const orderCountFor = (p: LtDetailedAccountPosition): number =>
165
172
  */
166
173
  const TX_HASH_PATTERN = /^[0-9a-f]{80}$/
167
174
 
175
+ /**
176
+ * Lighter's `client_order_index` is an int64 and `accountOrders` takes it as a
177
+ * decimal string, so a prefixed id's suffix goes to the venue verbatim rather
178
+ * than through a lossy JS number.
179
+ */
180
+ const CLIENT_ORDER_INDEX_PATTERN = /^\d+$/
181
+
168
182
  const INACTIVE_ORDERS_LOOKUP_LIMIT = 100
169
183
 
170
184
  /** Activity surfaces whose rows name a market, so they need the market registry. */
@@ -255,6 +269,12 @@ interface CachedStandardToken {
255
269
  token: string
256
270
  /** Unix seconds — re-create when `Date.now()/1000 + renewBuffer >= expiresAt`. */
257
271
  expiresAt: number
272
+ /**
273
+ * Slot and key material that signed `token`, as
274
+ * `apiKeyIndex:accountIndex:apiKeyPrivateKey`. `REGISTER_API_KEY` rotates the
275
+ * stored key, and Lighter rejects a token the superseded key signed.
276
+ */
277
+ signingKey: string
258
278
  }
259
279
 
260
280
  /**
@@ -359,10 +379,12 @@ export const createLighterProvider = (
359
379
  indices: { apiKeyIndex: number; accountIndex: number }
360
380
  ): Promise<string> => {
361
381
  const cacheKey = address.toLowerCase()
382
+ const signingKey = `${indices.apiKeyIndex}:${indices.accountIndex}:${apiKeyPrivateKey}`
362
383
  const nowSec = Math.floor(Date.now() / 1000)
363
384
  const cached = standardTokenByAddress.get(cacheKey)
364
385
  if (
365
386
  cached !== undefined &&
387
+ cached.signingKey === signingKey &&
366
388
  cached.expiresAt - nowSec > tokenRenewBufferSeconds
367
389
  ) {
368
390
  return cached.token
@@ -377,7 +399,7 @@ export const createLighterProvider = (
377
399
  lifetimeSeconds: tokenLifetimeSeconds,
378
400
  })
379
401
  const expiresAt = nowSec + tokenLifetimeSeconds
380
- standardTokenByAddress.set(cacheKey, { token, expiresAt })
402
+ standardTokenByAddress.set(cacheKey, { token, expiresAt, signingKey })
381
403
  return token
382
404
  }
383
405
 
@@ -415,11 +437,17 @@ export const createLighterProvider = (
415
437
  return undefined
416
438
  }
417
439
 
418
- const standardToken = (): Promise<string> =>
419
- getStandardAuthToken(address, apiKey.apiKeyPrivateKey, {
420
- apiKeyIndex: apiKey.apiKeyIndex,
421
- accountIndex: apiKey.accountIndex,
440
+ // Sign with the key registered at call time, not the one read on entry:
441
+ // `REGISTER_API_KEY` can rotate the store while an await below is pending,
442
+ // and Lighter rejects a token the superseded key signed. The entry value
443
+ // stands in only when the record is gone, which no key can re-sign.
444
+ const standardToken = async (): Promise<string> => {
445
+ const current = (await keyStore.get(address)) ?? apiKey
446
+ return getStandardAuthToken(address, current.apiKeyPrivateKey, {
447
+ apiKeyIndex: current.apiKeyIndex,
448
+ accountIndex: current.accountIndex,
422
449
  })
450
+ }
423
451
 
424
452
  const stored = await readOnlyTokenManager.get(address, apiKey.accountIndex)
425
453
  if (stored !== undefined) {
@@ -567,7 +595,7 @@ export const createLighterProvider = (
567
595
  account_index: accountIndex,
568
596
  })
569
597
 
570
- const fetchActiveOrdersForMarket = async (
598
+ const fetchActiveOrders = async (
571
599
  client: LighterApiClient,
572
600
  authToken: string,
573
601
  accountIndex: number,
@@ -581,6 +609,20 @@ export const createLighterProvider = (
581
609
  return { ...response, orders: wireList(response.orders) }
582
610
  }
583
611
 
612
+ const fetchOrdersByClientOrderIndex = async (
613
+ client: LighterApiClient,
614
+ authToken: string,
615
+ accountIndex: number,
616
+ clientOrderIndex: string
617
+ ): Promise<LtOrder[]> => {
618
+ const response = await client.getAuthed<LtAccountOrdersResponse>(
619
+ '/api/v1/accountOrders',
620
+ authToken,
621
+ { account_index: accountIndex, client_order_indexes: clientOrderIndex }
622
+ )
623
+ return wireList(response.orders)
624
+ }
625
+
584
626
  const deriveOrderBearingMarketIds = (account: LtAccount): number[] =>
585
627
  account.positions
586
628
  .filter((p) => orderCountFor(p) > 0)
@@ -863,7 +905,8 @@ export const createLighterProvider = (
863
905
  apiKeyRegistered,
864
906
  accountType: account.account_type,
865
907
  userTierName: limitsResult?.user_tier_name,
866
- accountTradingMode: account.account_trading_mode,
908
+ accountTradingMode:
909
+ account.account_trading_mode ?? LT_ACCOUNT_TRADING_MODE_SIMPLE,
867
910
  assetCollateral,
868
911
  // Satisfied when a non-expired read-only token is stored locally
869
912
  // (`readOnlyTokenManager.get` filters out expired ones).
@@ -1017,21 +1060,17 @@ export const createLighterProvider = (
1017
1060
  registry.sync(),
1018
1061
  ])
1019
1062
 
1020
- const marketIds =
1063
+ const marketId =
1021
1064
  params.marketId === undefined
1022
- ? deriveOrderBearingMarketIds(account)
1023
- : [Number(registry.require(params.marketId).id)]
1065
+ ? LIGHTER_ALL_MARKETS_WILDCARD
1066
+ : Number(registry.require(params.marketId).id)
1024
1067
 
1025
- const responses = await retryOnRevoked(opts, params.address, token, (t) =>
1026
- Promise.all(
1027
- marketIds.map((id) =>
1028
- fetchActiveOrdersForMarket(client, t, account.index, id)
1029
- )
1030
- )
1068
+ const response = await retryOnRevoked(opts, params.address, token, (t) =>
1069
+ fetchActiveOrders(client, t, account.index, marketId)
1031
1070
  )
1032
1071
 
1033
1072
  const { openOrders, triggerOrders } = classifyAndMapOrders(
1034
- responses.flatMap((r) => r.orders),
1073
+ response.orders,
1035
1074
  (marketIndex) => registry.require(String(marketIndex))
1036
1075
  )
1037
1076
 
@@ -1065,9 +1104,7 @@ export const createLighterProvider = (
1065
1104
  : [Number(registry.require(params.marketId).id)]
1066
1105
  const responses = await retryOnRevoked(opts, params.address, token, (t) =>
1067
1106
  Promise.all(
1068
- marketIds.map((id) =>
1069
- fetchActiveOrdersForMarket(client, t, account.index, id)
1070
- )
1107
+ marketIds.map((id) => fetchActiveOrders(client, t, account.index, id))
1071
1108
  )
1072
1109
  )
1073
1110
 
@@ -1087,6 +1124,14 @@ export const createLighterProvider = (
1087
1124
  return twaps
1088
1125
  },
1089
1126
 
1127
+ /**
1128
+ * Resolves either reference Lighter hands out for an order: a bare
1129
+ * `order_index` — the value `Order.orderId` carries, as streamed by
1130
+ * `orderUpdates` / `fills` — or a `client_order_index` prefixed with
1131
+ * {@link LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}, which a signed placement
1132
+ * step returns and which resolves before the venue has assigned an
1133
+ * `order_index`.
1134
+ */
1090
1135
  async getOrder(
1091
1136
  params: ProviderGetOrderParams,
1092
1137
  opts?: SDKRequestOptions
@@ -1108,41 +1153,75 @@ export const createLighterProvider = (
1108
1153
  registry.sync(),
1109
1154
  ])
1110
1155
 
1111
- // Native `order_index` route only. The cross-provider `Order.orderId`
1112
- // for Lighter is `String(order_index)` — see `mapOrder`. A tx-hash route
1113
- // would require mapping the caller's executeAction tx hash → wasm nonce
1114
- // → matching order, which the LI.FI backend did via its `UserAction`
1115
- // table; the SDK has no equivalent persistence, so we refuse rather
1116
- // than mis-resolve.
1156
+ const notFound = (): PerpsError =>
1157
+ new PerpsError(
1158
+ PerpsErrorCode.OrderNotFound,
1159
+ `Lighter order ${params.id} not found for ${params.address}`
1160
+ )
1161
+ const detail = (order: LtOrder): Order =>
1162
+ mapOrderDetail(order, registry.require(String(order.market_index)))
1163
+
1164
+ // A tx-hash route would require mapping the caller's executeAction tx
1165
+ // hash → wasm nonce → matching order, which the LI.FI backend did via
1166
+ // its `UserAction` table; the SDK has no equivalent persistence, so we
1167
+ // refuse rather than mis-resolve.
1117
1168
  if (TX_HASH_PATTERN.test(params.id)) {
1118
1169
  throw new PerpsError(
1119
1170
  PerpsErrorCode.OrderNotFound,
1120
1171
  `Lighter order id "${params.id}" looks like a tx hash. The SDK ` +
1121
- `resolves orders by Lighter \`order_index\` only — surface the orderId ` +
1122
- `from the orderUpdates / fills WS stream and pass it here.`
1172
+ `resolves orders by Lighter \`order_index\` or by a ` +
1173
+ `\`${LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}<index>\` id — surface the ` +
1174
+ `orderId from the orderUpdates / fills WS stream, or the ` +
1175
+ `clientOrderIndex from the signed placement step, and pass it here.`
1123
1176
  )
1124
1177
  }
1125
- const predicate: (o: { order_index: number }) => boolean = (o) =>
1126
- String(o.order_index) === params.id
1127
1178
 
1128
- const marketIds = deriveOrderBearingMarketIds(account)
1129
- const activeResponses = await retryOnRevoked(
1130
- opts,
1131
- params.address,
1132
- token,
1133
- (t) =>
1134
- Promise.all(
1135
- marketIds.map((id) =>
1136
- fetchActiveOrdersForMarket(client, t, account.index, id)
1137
- )
1179
+ if (params.id.startsWith(LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX)) {
1180
+ const clientOrderIndex = params.id.slice(
1181
+ LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX.length
1182
+ )
1183
+ if (!CLIENT_ORDER_INDEX_PATTERN.test(clientOrderIndex)) {
1184
+ throw new PerpsError(
1185
+ PerpsErrorCode.SDKError,
1186
+ `Lighter order id "${params.id}" names a client order index, but ` +
1187
+ `"${clientOrderIndex}" is not a decimal integer.`
1138
1188
  )
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
1189
  }
1190
+ const orders = await retryOnRevoked(opts, params.address, token, (t) =>
1191
+ fetchOrdersByClientOrderIndex(
1192
+ client,
1193
+ t,
1194
+ account.index,
1195
+ clientOrderIndex
1196
+ )
1197
+ )
1198
+ // `accountOrders` reaches active and inactive rows alike, so a reused
1199
+ // client order index can match more than one row. Prefer the live one.
1200
+ const matches = orders
1201
+ .filter((o) => String(o.client_order_index) === clientOrderIndex)
1202
+ .map(detail)
1203
+ const hit =
1204
+ matches.find((o) => isActiveOrderStatus(o.status)) ?? matches[0]
1205
+ if (hit === undefined) {
1206
+ throw notFound()
1207
+ }
1208
+ return hit
1209
+ }
1210
+
1211
+ const byOrderIndex = (o: LtOrder): boolean =>
1212
+ String(o.order_index) === params.id
1213
+
1214
+ const active = await retryOnRevoked(opts, params.address, token, (t) =>
1215
+ fetchActiveOrders(
1216
+ client,
1217
+ t,
1218
+ account.index,
1219
+ LIGHTER_ALL_MARKETS_WILDCARD
1220
+ )
1221
+ )
1222
+ const activeHit = active.orders.find(byOrderIndex)
1223
+ if (activeHit !== undefined) {
1224
+ return detail(activeHit)
1146
1225
  }
1147
1226
 
1148
1227
  const inactive = await retryOnRevoked(opts, params.address, token, (t) =>
@@ -1152,17 +1231,11 @@ export const createLighterProvider = (
1152
1231
  limit: INACTIVE_ORDERS_LOOKUP_LIMIT,
1153
1232
  })
1154
1233
  )
1155
- const hit = wireList(inactive.orders).find(
1156
- predicate as (o: unknown) => boolean
1157
- )
1158
- if (hit !== undefined) {
1159
- return mapOrderDetail(hit, registry.require(String(hit.market_index)))
1234
+ const inactiveHit = wireList(inactive.orders).find(byOrderIndex)
1235
+ if (inactiveHit === undefined) {
1236
+ throw notFound()
1160
1237
  }
1161
-
1162
- throw new PerpsError(
1163
- PerpsErrorCode.OrderNotFound,
1164
- `Lighter order ${params.id} not found for ${params.address}`
1165
- )
1238
+ return detail(inactiveHit)
1166
1239
  },
1167
1240
 
1168
1241
  async getFills(
@@ -5,6 +5,10 @@ import type {
5
5
  ProviderAction,
6
6
  } from '@lifi/perps-types'
7
7
  import { ActionType, PerpsErrorCode } from '@lifi/perps-types'
8
+ import {
9
+ LT_ACCOUNT_TRADING_MODE_SIMPLE,
10
+ LT_ACCOUNT_TRADING_MODE_UNIFIED,
11
+ } from './types/action.js'
8
12
 
9
13
  function assertNever(value: never): never {
10
14
  throw new Error(
@@ -27,15 +31,11 @@ const ACCOUNT_TYPE_INT_TO_WIRE: Readonly<Record<number, string>> = {
27
31
  [LIGHTER_ACCOUNT_TYPE_PREMIUM]: 'premium',
28
32
  }
29
33
 
30
- // `account_trading_mode` on `DetailedAccount`: 0 = Classic/Simple (segregated
31
- // margin), 1 = Unified Trading Account (cross-asset margin). Wire strings match
32
- // the backend descriptor's ParamOption values. An unmapped int projects to null.
33
- const LIGHTER_ACCOUNT_MODE_SIMPLE = 0
34
- const LIGHTER_ACCOUNT_MODE_UNIFIED = 1
35
-
34
+ // Wire strings match the backend descriptor's ParamOption values. An unmapped
35
+ // int projects to null.
36
36
  const ACCOUNT_MODE_INT_TO_WIRE: Readonly<Record<number, string>> = {
37
- [LIGHTER_ACCOUNT_MODE_SIMPLE]: 'simpleTradingAccount',
38
- [LIGHTER_ACCOUNT_MODE_UNIFIED]: 'unifiedTradingAccount',
37
+ [LT_ACCOUNT_TRADING_MODE_SIMPLE]: 'simpleTradingAccount',
38
+ [LT_ACCOUNT_TRADING_MODE_UNIFIED]: 'unifiedTradingAccount',
39
39
  }
40
40
 
41
41
  /**
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 `accountInactiveOrders` and
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
  *
@@ -235,6 +248,16 @@ const LIGHTER_INVALID_AUTH_CODE = 20013
235
248
  */
236
249
  const LIGHTER_TOKEN_REVOKED_CODE = 61006
237
250
 
251
+ /**
252
+ * Lighter body `code` for the internal-error envelope it answers when a token's
253
+ * signature does not verify — the message reads `invalid signature`. Only the
254
+ * auth-gated read path treats it as an auth failure; a public read keeps the
255
+ * venue-error classification.
256
+ *
257
+ * @internal
258
+ */
259
+ const LIGHTER_INVALID_SIGNATURE_CODE = 29500
260
+
238
261
  /**
239
262
  * Lighter body `code` values that mean success. Lighter is inconsistent per
240
263
  * endpoint: `/api/v1/account` returns `code: 200`, most others return `code: 0`.
@@ -266,6 +289,7 @@ const LIGHTER_REFERRAL_ALREADY_USED_CODE = 41003
266
289
  export {
267
290
  LIGHTER_CODE_ACCOUNT_NOT_FOUND,
268
291
  LIGHTER_INVALID_AUTH_CODE,
292
+ LIGHTER_INVALID_SIGNATURE_CODE,
269
293
  LIGHTER_MUTATION_SUCCESS_CODE,
270
294
  LIGHTER_REFERRAL_ALREADY_USED_CODE,
271
295
  LIGHTER_SUCCESS_CODES,
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export {
23
23
  DEFAULT_LIGHTER_EXPLORER_TX_BASE_URL,
24
24
  DEFAULT_LIGHTER_REST_URL,
25
25
  DEFAULT_LIGHTER_WS_URL,
26
+ LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX,
26
27
  LIGHTER_COLLATERAL_ASSETS,
27
28
  LIGHTER_MAINNET_DEPLOYMENT,
28
29
  LIGHTER_MAINNET_SIGNER_CHAIN_ID,
@@ -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, authToken)
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
- * form body, mirroring the backend's request contract. The auth token is the
538
- * `auth` field; the remaining fields come from `wasmSignParams`.
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
- step: WasmBlobActionStep,
542
- authToken: string
543
- ): { path: string; params: ApiParams } {
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: { auth: authToken, account_index, new_tier },
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: { auth: authToken, l1_address, referral_code, x },
592
+ params: { l1_address, referral_code, x },
576
593
  }
577
594
  }
578
595
  throw new PerpsError(
@@ -30,12 +30,18 @@ export type LtAccountPosition = {
30
30
  /**
31
31
  * Funding accrued since the position opened, signed from the account's point
32
32
  * of view despite the field name: negative is funding the account paid and
33
- * positive is funding it received. Lighter resets it to `"0"` on close.
33
+ * positive is funding it received. Lighter resets it to `"0"` on close, and
34
+ * marks it `omitempty`, so a position that accrued no funding yet omits the
35
+ * field entirely; an absent value means zero.
34
36
  */
35
- total_funding_paid_out: string
37
+ total_funding_paid_out?: string
36
38
  margin_mode: number
37
39
  allocated_margin: string
38
- total_discount: string
40
+ /**
41
+ * Funding-fee discount accrued on the position. Lighter marks it
42
+ * `omitempty`, so an absent field means zero.
43
+ */
44
+ total_discount?: string
39
45
  /** Lighter bit flags recording which margin settings the account set itself. */
40
46
  margin_set_flag?: number
41
47
  }
@@ -125,7 +131,12 @@ export interface LtDetailedAccount {
125
131
  * treat `0` as "no transaction" and not as the Unix epoch.
126
132
  */
127
133
  transaction_time: number
128
- account_trading_mode: number
134
+ /**
135
+ * Lighter marks it `omitempty` here, so an absent field means
136
+ * `LT_ACCOUNT_TRADING_MODE_SIMPLE`, the wire zero value. `LtSubAccount`
137
+ * reports the same member without the omission.
138
+ */
139
+ account_trading_mode?: number
129
140
  account_index: number
130
141
  name: string
131
142
  description: string
@@ -184,6 +184,21 @@ export const LT_MARGIN_MODE_CROSS = 0
184
184
  */
185
185
  export const LT_MARGIN_MODE_ISOLATED = 1
186
186
 
187
+ /**
188
+ * Lighter account-trading-mode value for the Classic/Simple account, which
189
+ * segregates margin per market. The wire zero value, so an omitted
190
+ * `account_trading_mode` means this mode.
191
+ * @public
192
+ */
193
+ export const LT_ACCOUNT_TRADING_MODE_SIMPLE = 0
194
+
195
+ /**
196
+ * Lighter account-trading-mode value for the Unified Trading Account, which
197
+ * margins across assets.
198
+ * @public
199
+ */
200
+ export const LT_ACCOUNT_TRADING_MODE_UNIFIED = 1
201
+
187
202
  /**
188
203
  * Lighter margin-direction value for removing margin.
189
204
  * @public
package/src/types/fill.ts CHANGED
@@ -41,8 +41,9 @@ export type LtTrade = {
41
41
  transaction_time: number
42
42
  // Per-counterparty position snapshot BEFORE the trade is applied. Signed
43
43
  // strings: positive = long, negative = short, "0" / "0.00000" = flat.
44
- taker_position_size_before: string
45
- maker_position_size_before: string
44
+ // Lighter marks both `omitempty`, so an absent key means flat.
45
+ taker_position_size_before?: string
46
+ maker_position_size_before?: string
46
47
  // Per-counterparty entry-quote (notional cost basis) snapshot BEFORE the
47
48
  // trade. Paired with `*_position_size_before`, avg entry =
48
49
  // entry_quote_before / |position_size_before|. Optional: absent on older
@@ -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