@lifi/perps-sdk-provider-lighter 24.0.2 → 26.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 (117) hide show
  1. package/README.md +55 -0
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +71 -56
  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 +1 -7
  7. package/dist/cjs/accountConfig.js.map +1 -1
  8. package/dist/cjs/constants.d.ts +2 -0
  9. package/dist/cjs/constants.d.ts.map +1 -1
  10. package/dist/cjs/constants.js +3 -0
  11. package/dist/cjs/constants.js.map +1 -1
  12. package/dist/cjs/signers/LighterSigner.d.ts.map +1 -1
  13. package/dist/cjs/signers/LighterSigner.js +2 -1
  14. package/dist/cjs/signers/LighterSigner.js.map +1 -1
  15. package/dist/cjs/types/submit.d.ts.map +1 -1
  16. package/dist/cjs/utils/activityCursor.d.ts.map +1 -1
  17. package/dist/cjs/utils/activityCursor.js +29 -6
  18. package/dist/cjs/utils/activityCursor.js.map +1 -1
  19. package/dist/cjs/utils/index.d.ts +1 -1
  20. package/dist/cjs/utils/index.d.ts.map +1 -1
  21. package/dist/cjs/utils/index.js +2 -7
  22. package/dist/cjs/utils/index.js.map +1 -1
  23. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapFill.js +4 -2
  25. package/dist/cjs/utils/mapFill.js.map +1 -1
  26. package/dist/cjs/utils/mapOrder.d.ts +4 -11
  27. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  28. package/dist/cjs/utils/mapOrder.js +112 -120
  29. package/dist/cjs/utils/mapOrder.js.map +1 -1
  30. package/dist/cjs/utils/txHash.d.ts +2 -0
  31. package/dist/cjs/utils/txHash.d.ts.map +1 -0
  32. package/dist/cjs/utils/txHash.js +10 -0
  33. package/dist/cjs/utils/txHash.js.map +1 -0
  34. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  35. package/dist/cjs/websocket/LighterWsProvider.js +11 -9
  36. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  37. package/dist/esm/LighterProvider.d.ts.map +1 -1
  38. package/dist/esm/LighterProvider.js +78 -65
  39. package/dist/esm/LighterProvider.js.map +1 -1
  40. package/dist/esm/accountConfig.d.ts.map +1 -1
  41. package/dist/esm/accountConfig.js +5 -16
  42. package/dist/esm/accountConfig.js.map +1 -1
  43. package/dist/esm/constants.d.ts +9 -3
  44. package/dist/esm/constants.d.ts.map +1 -1
  45. package/dist/esm/constants.js +6 -3
  46. package/dist/esm/constants.js.map +1 -1
  47. package/dist/esm/signers/LighterSigner.d.ts.map +1 -1
  48. package/dist/esm/signers/LighterSigner.js +2 -1
  49. package/dist/esm/signers/LighterSigner.js.map +1 -1
  50. package/dist/esm/types/submit.d.ts +16 -6
  51. package/dist/esm/types/submit.d.ts.map +1 -1
  52. package/dist/esm/types/ws.d.ts +1 -1
  53. package/dist/esm/utils/activityCursor.d.ts +2 -11
  54. package/dist/esm/utils/activityCursor.d.ts.map +1 -1
  55. package/dist/esm/utils/activityCursor.js +35 -7
  56. package/dist/esm/utils/activityCursor.js.map +1 -1
  57. package/dist/esm/utils/index.d.ts +1 -1
  58. package/dist/esm/utils/index.d.ts.map +1 -1
  59. package/dist/esm/utils/index.js +1 -1
  60. package/dist/esm/utils/index.js.map +1 -1
  61. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  62. package/dist/esm/utils/mapFill.js +5 -3
  63. package/dist/esm/utils/mapFill.js.map +1 -1
  64. package/dist/esm/utils/mapOrder.d.ts +6 -68
  65. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  66. package/dist/esm/utils/mapOrder.js +111 -180
  67. package/dist/esm/utils/mapOrder.js.map +1 -1
  68. package/dist/esm/utils/txHash.d.ts +8 -0
  69. package/dist/esm/utils/txHash.d.ts.map +1 -0
  70. package/dist/esm/utils/txHash.js +18 -0
  71. package/dist/esm/utils/txHash.js.map +1 -0
  72. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  73. package/dist/esm/websocket/LighterWsProvider.js +20 -16
  74. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  75. package/dist/types/LighterProvider.d.ts.map +1 -1
  76. package/dist/types/accountConfig.d.ts.map +1 -1
  77. package/dist/types/constants.d.ts +9 -3
  78. package/dist/types/constants.d.ts.map +1 -1
  79. package/dist/types/signers/LighterSigner.d.ts.map +1 -1
  80. package/dist/types/types/submit.d.ts +16 -6
  81. package/dist/types/types/submit.d.ts.map +1 -1
  82. package/dist/types/types/ws.d.ts +1 -1
  83. package/dist/types/utils/activityCursor.d.ts +2 -11
  84. package/dist/types/utils/activityCursor.d.ts.map +1 -1
  85. package/dist/types/utils/index.d.ts +1 -1
  86. package/dist/types/utils/index.d.ts.map +1 -1
  87. package/dist/types/utils/mapFill.d.ts.map +1 -1
  88. package/dist/types/utils/mapOrder.d.ts +6 -68
  89. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  90. package/dist/types/utils/txHash.d.ts +8 -0
  91. package/dist/types/utils/txHash.d.ts.map +1 -0
  92. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  93. package/dist/wasm/lighter-signer.wasm +0 -0
  94. package/package.json +3 -3
  95. package/src/LighterProvider.ts +87 -98
  96. package/src/accountConfig.ts +5 -18
  97. package/src/constants.ts +11 -3
  98. package/src/signers/LighterSigner.ts +2 -0
  99. package/src/types/submit.ts +16 -6
  100. package/src/types/ws.ts +1 -1
  101. package/src/utils/activityCursor.ts +50 -18
  102. package/src/utils/index.ts +1 -9
  103. package/src/utils/mapFill.ts +5 -8
  104. package/src/utils/mapOrder.ts +130 -207
  105. package/src/utils/txHash.ts +20 -0
  106. package/src/websocket/LighterWsProvider.ts +22 -17
  107. package/dist/cjs/utils/mapTwap.d.ts +0 -4
  108. package/dist/cjs/utils/mapTwap.d.ts.map +0 -1
  109. package/dist/cjs/utils/mapTwap.js +0 -31
  110. package/dist/cjs/utils/mapTwap.js.map +0 -1
  111. package/dist/esm/utils/mapTwap.d.ts +0 -5
  112. package/dist/esm/utils/mapTwap.d.ts.map +0 -1
  113. package/dist/esm/utils/mapTwap.js +0 -25
  114. package/dist/esm/utils/mapTwap.js.map +0 -1
  115. package/dist/types/utils/mapTwap.d.ts +0 -5
  116. package/dist/types/utils/mapTwap.d.ts.map +0 -1
  117. package/src/utils/mapTwap.ts +0 -31
@@ -1,5 +1,7 @@
1
1
  import {
2
+ ACTIVE_ORDER_STATUSES,
2
3
  type DepositFlow,
4
+ explorerTxUrl,
3
5
  explorerTxUrlFromBase,
4
6
  getAssetRegistry,
5
7
  getMarketRegistry,
@@ -20,7 +22,6 @@ import {
20
22
  type ProviderGetPortfolioHistoryParams,
21
23
  type ProviderGetPositionsParams,
22
24
  type ProviderGetQuoteParams,
23
- type ProviderGetRunningTwapsParams,
24
25
  type ProviderGetWithdrawableBalancesParams,
25
26
  type ProviderWithdrawableBalance,
26
27
  paginateActivity,
@@ -55,7 +56,6 @@ import type {
55
56
  Quote,
56
57
  SignedActionStep,
57
58
  SigningMethod,
58
- TwapOrder,
59
59
  } from '@lifi/perps-types'
60
60
  import {
61
61
  ActionType,
@@ -97,7 +97,6 @@ import type {
97
97
  LtAccountPnL,
98
98
  LtDepositHistoryItem,
99
99
  LtDepositHistoryResponse,
100
- LtDetailedAccountPosition,
101
100
  LtLiquidation,
102
101
  LtLiquidationsResponse,
103
102
  LtOrder,
@@ -128,7 +127,6 @@ import {
128
127
  } from './utils/apiClient.js'
129
128
  import { isAssetMarginEnabled } from './utils/assetCollateral.js'
130
129
  import {
131
- classifyAndMapOrders,
132
130
  estimateLiquidationPrice,
133
131
  fetchDetailedAccount,
134
132
  formatOrderPrice,
@@ -138,7 +136,7 @@ import {
138
136
  lighterWithdrawableBalances,
139
137
  mapFill,
140
138
  mapOpenPositions,
141
- mapOrderDetail,
139
+ mapOrder,
142
140
  mapPortfolioHistory,
143
141
  positionMarginConstraints,
144
142
  toBigOrNull,
@@ -146,11 +144,11 @@ import {
146
144
  toIsoFromSeconds,
147
145
  toRequiredBig,
148
146
  } from './utils/index.js'
149
- import { mapRunningTwap } from './utils/mapTwap.js'
150
147
  import {
151
148
  fetchRegisteredApiKey,
152
149
  normalizeLighterPublicKey,
153
150
  } from './utils/registeredApiKey.js'
151
+ import { isPlaceholderTxHash } from './utils/txHash.js'
154
152
  import { wireList } from './utils/wireList.js'
155
153
 
156
154
  const ZERO_FEE_TIER = { maker: '0', taker: '0' }
@@ -165,11 +163,6 @@ const projectFeeTier = (
165
163
  taker: tickToFeeString(limits.current_taker_fee_tick),
166
164
  })
167
165
 
168
- const orderCountFor = (p: LtDetailedAccountPosition): number =>
169
- (p.open_order_count ?? 0) +
170
- (p.pending_order_count ?? 0) +
171
- (p.position_tied_order_count ?? 0)
172
-
173
166
  /**
174
167
  * Lighter `sendTx` returns a bare lowercase-hex tx hash (40 bytes → 80 hex
175
168
  * chars). `Order.order_id` is a different identifier the matching engine
@@ -349,6 +342,7 @@ export const createLighterProvider = (
349
342
  const providerKey = deployment.providerKey
350
343
  const restUrl = options.restUrl ?? deployment.restUrl
351
344
  const explorerTxBaseUrl = deployment.explorerTxBaseUrl
345
+ const bridgeChainId = deployment.bridgeChainId
352
346
  const collateral = deployment.collateral
353
347
  const authTokenSource: (() => string | Promise<string>) | undefined =
354
348
  typeof options.authToken === 'function'
@@ -647,11 +641,6 @@ export const createLighterProvider = (
647
641
  return wireList(response.orders)
648
642
  }
649
643
 
650
- const deriveOrderBearingMarketIds = (account: LtAccount): number[] =>
651
- account.positions
652
- .filter((p) => orderCountFor(p) > 0)
653
- .map((p) => p.market_id)
654
-
655
644
  const fetchAllHistory = async (
656
645
  client: LighterApiClient,
657
646
  token: string,
@@ -897,23 +886,26 @@ export const createLighterProvider = (
897
886
  ),
898
887
  units: availableMargin.toString(),
899
888
  valueUsd: availableMargin.toString(),
889
+ price: '1',
900
890
  },
901
891
  ]
902
892
  : []
903
893
  // Spot token holdings — non-collateral. The instance's settlement asset
904
894
  // is valued 1:1; other tokens have no price source at this boundary, so
905
- // their USD value is unknown.
895
+ // their price and USD value are unknown.
906
896
  const balances: Balance[] = account.assets
907
897
  .filter((asset) => toRequiredBig(asset.balance, 'balance').gt(0))
908
898
  .map((a) => {
909
899
  const assetId = String(a.asset_id)
900
+ const settlement = a.asset_id === collateral.assetIndex
910
901
  return {
911
902
  categoryId: spotCategoryId,
912
903
  asset:
913
904
  assetRegistry.get(assetId) ??
914
905
  lighterAsset(assetId, a.symbol, providerKey),
915
906
  units: a.balance,
916
- valueUsd: a.asset_id === collateral.assetIndex ? a.balance : '0',
907
+ valueUsd: settlement ? a.balance : '0',
908
+ ...(settlement ? { price: '1' } : {}),
917
909
  }
918
910
  })
919
911
 
@@ -1063,24 +1055,22 @@ export const createLighterProvider = (
1063
1055
  }
1064
1056
  },
1065
1057
 
1066
- /**
1067
- * Lighter's `accountActiveOrders` endpoint takes no limit/cursor and
1068
- * returns every active order for the account, so the response is always
1069
- * the complete set: `params.limit` is not honoured and `pagination` is
1070
- * reported as `{ limit: <count returned>, hasMore: false }` with no cursor.
1071
- */
1072
1058
  async getOrders(
1073
1059
  params: ProviderGetOrdersParams,
1074
1060
  opts?: SDKRequestOptions
1075
1061
  ): Promise<OrdersResponse> {
1062
+ const statuses = new Set(params.statuses ?? ACTIVE_ORDER_STATUSES)
1063
+ const empty: OrdersResponse = {
1064
+ provider: providerKey,
1065
+ orders: [],
1066
+ pagination: { limit: params.limit ?? 0, hasMore: false },
1067
+ }
1068
+ if (statuses.size === 0) {
1069
+ return empty
1070
+ }
1076
1071
  const token = await resolveAuthToken(opts, params.address)
1077
1072
  if (token === undefined) {
1078
- return {
1079
- provider: providerKey,
1080
- openOrders: [],
1081
- triggerOrders: [],
1082
- pagination: { limit: params.limit ?? 0, hasMore: false },
1083
- }
1073
+ return empty
1084
1074
  }
1085
1075
 
1086
1076
  const client = apiClient(opts)
@@ -1089,69 +1079,73 @@ export const createLighterProvider = (
1089
1079
  fetchDetailedAccount(client, params.address),
1090
1080
  registry.sync(),
1091
1081
  ])
1092
-
1093
1082
  const marketId =
1094
1083
  params.marketId === undefined
1095
1084
  ? LIGHTER_ALL_MARKETS_WILDCARD
1096
1085
  : Number(registry.require(params.marketId).id)
1097
-
1098
- const response = await retryOnRevoked(opts, params.address, token, (t) =>
1099
- fetchActiveOrders(client, t, account.index, marketId)
1100
- )
1101
-
1102
- const { openOrders, triggerOrders } = classifyAndMapOrders(
1103
- response.orders,
1104
- (marketIndex) => registry.require(String(marketIndex))
1105
- )
1106
-
1107
- const total = openOrders.length + triggerOrders.length
1108
- return {
1109
- provider: providerKey,
1110
- openOrders,
1111
- triggerOrders,
1112
- pagination: { limit: total, hasMore: false },
1113
- }
1114
- },
1115
-
1116
- async getRunningTwaps(
1117
- params: ProviderGetRunningTwapsParams,
1118
- opts?: SDKRequestOptions
1119
- ): Promise<TwapOrder[]> {
1120
- const token = await resolveAuthToken(opts, params.address)
1121
- if (token === undefined) {
1122
- return []
1086
+ let active = false
1087
+ let terminal = false
1088
+ for (const status of statuses) {
1089
+ if (isActiveOrderStatus(status)) {
1090
+ active = true
1091
+ } else {
1092
+ terminal = true
1093
+ }
1123
1094
  }
1124
-
1125
- const client = apiClient(opts)
1126
- const registry = getMarketRegistry(requireClient(), providerKey)
1127
- const [account] = await Promise.all([
1128
- fetchDetailedAccount(client, params.address),
1129
- registry.sync(),
1130
- ])
1131
- const marketIds =
1132
- params.marketId === undefined
1133
- ? deriveOrderBearingMarketIds(account)
1134
- : [Number(registry.require(params.marketId).id)]
1135
- const responses = await retryOnRevoked(opts, params.address, token, (t) =>
1136
- Promise.all(
1137
- marketIds.map((id) => fetchActiveOrders(client, t, account.index, id))
1138
- )
1095
+ const [activeResponse, inactiveResponse] = await retryOnRevoked(
1096
+ opts,
1097
+ params.address,
1098
+ token,
1099
+ (t) =>
1100
+ Promise.all([
1101
+ active && params.cursor === undefined
1102
+ ? fetchActiveOrders(client, t, account.index, marketId)
1103
+ : undefined,
1104
+ terminal
1105
+ ? client.getAuthed<LtOrdersResponse>(
1106
+ '/api/v1/accountInactiveOrders',
1107
+ t,
1108
+ {
1109
+ account_index: account.index,
1110
+ market_id: marketId,
1111
+ limit: params.limit ?? LIGHTER_HISTORY_PAGE_SIZE,
1112
+ ...(params.cursor === undefined
1113
+ ? {}
1114
+ : { cursor: params.cursor }),
1115
+ }
1116
+ )
1117
+ : undefined,
1118
+ ])
1139
1119
  )
1140
-
1141
- const twaps: TwapOrder[] = []
1142
- for (const response of responses) {
1143
- for (const order of response.orders) {
1144
- if (order.type === 'twap') {
1145
- twaps.push(
1146
- mapRunningTwap(
1147
- order,
1148
- registry.require(String(order.market_index))
1149
- )
1150
- )
1120
+ const orders: Order[] = []
1121
+ for (const response of [activeResponse, inactiveResponse]) {
1122
+ for (const raw of wireList(response?.orders)) {
1123
+ // `get`, not `require`: a market id the backend list no longer
1124
+ // carries drops only its own row instead of rejecting the whole
1125
+ // page. The registry warns once per unresolved id. A delisted
1126
+ // market still resolves, so its rows stay.
1127
+ const market = registry.get(String(raw.market_index))
1128
+ if (market === undefined) {
1129
+ continue
1130
+ }
1131
+ const order = mapOrder(raw, market)
1132
+ if (statuses.has(order.status)) {
1133
+ orders.push(order)
1151
1134
  }
1152
1135
  }
1153
1136
  }
1154
- return twaps
1137
+ const cursor = inactiveResponse?.next_cursor || undefined
1138
+ return {
1139
+ provider: providerKey,
1140
+ orders,
1141
+ pagination: {
1142
+ limit: terminal
1143
+ ? (params.limit ?? LIGHTER_HISTORY_PAGE_SIZE)
1144
+ : orders.length,
1145
+ hasMore: cursor !== undefined,
1146
+ ...(cursor === undefined ? {} : { cursor }),
1147
+ },
1148
+ }
1155
1149
  },
1156
1150
 
1157
1151
  /**
@@ -1189,7 +1183,7 @@ export const createLighterProvider = (
1189
1183
  `Lighter order ${params.id} not found for ${params.address}`
1190
1184
  )
1191
1185
  const detail = (order: LtOrder): Order =>
1192
- mapOrderDetail(order, registry.require(String(order.market_index)))
1186
+ mapOrder(order, registry.require(String(order.market_index)))
1193
1187
 
1194
1188
  // A tx-hash route would require mapping the caller's executeAction tx
1195
1189
  // hash → wasm nonce → matching order, which the LI.FI backend did via
@@ -1433,9 +1427,6 @@ export const createLighterProvider = (
1433
1427
  : Promise.resolve(),
1434
1428
  ])
1435
1429
 
1436
- const assetSymbol = (assetId: number): string =>
1437
- assetRegistry.get(String(assetId))?.displaySymbol ?? String(assetId)
1438
-
1439
1430
  const items: ActivityItem[] = [
1440
1431
  ...history.deposits.deposits.map(
1441
1432
  (d): ActivityItem => ({
@@ -1443,11 +1434,9 @@ export const createLighterProvider = (
1443
1434
  provider: providerKey,
1444
1435
  timestamp: toIsoFromMs(d.timestamp),
1445
1436
  type: ActivityType.DEPOSIT,
1446
- asset: assetSymbol(d.asset_id),
1437
+ asset: assetRegistry.require(String(d.asset_id)),
1447
1438
  amount: d.amount,
1448
- explorerLink: d.l1_tx_hash
1449
- ? `https://scan.li.fi/tx/${d.l1_tx_hash}`
1450
- : undefined,
1439
+ explorerLink: explorerTxUrl(bridgeChainId, d.l1_tx_hash),
1451
1440
  })
1452
1441
  ),
1453
1442
  // `/withdraw/history` carries no fee field, so `fee` stays absent
@@ -1458,11 +1447,9 @@ export const createLighterProvider = (
1458
1447
  provider: providerKey,
1459
1448
  timestamp: toIsoFromMs(w.timestamp),
1460
1449
  type: ActivityType.WITHDRAWAL,
1461
- asset: assetSymbol(w.asset_id),
1450
+ asset: assetRegistry.require(String(w.asset_id)),
1462
1451
  amount: w.amount,
1463
- explorerLink: w.l1_tx_hash
1464
- ? `https://scan.li.fi/tx/${w.l1_tx_hash}`
1465
- : undefined,
1452
+ explorerLink: explorerTxUrl(bridgeChainId, w.l1_tx_hash),
1466
1453
  })
1467
1454
  ),
1468
1455
  // `get`, not `require`: a market id the backend list no longer carries
@@ -1555,13 +1542,15 @@ export const createLighterProvider = (
1555
1542
  type: ActivityType.TRANSFER,
1556
1543
  direction,
1557
1544
  counterpartyAccountIndex,
1558
- asset: assetSymbol(t.asset_id),
1545
+ asset: assetRegistry.require(String(t.asset_id)),
1559
1546
  amount: t.amount,
1560
1547
  // The venue charges the transfer fee in the deployment's
1561
1548
  // settlement asset, not in the asset the row moves, so a spot
1562
1549
  // token transfer still reports a collateral-denominated fee.
1563
1550
  fees: [{ amount: t.fee, asset: collateral.displaySymbol }],
1564
- explorerLink: explorerTxUrlFromBase(explorerTxBaseUrl, t.tx_hash),
1551
+ explorerLink: isPlaceholderTxHash(t.tx_hash)
1552
+ ? undefined
1553
+ : explorerTxUrlFromBase(explorerTxBaseUrl, t.tx_hash),
1565
1554
  meta: {
1566
1555
  transferType: t.type,
1567
1556
  txHash: t.tx_hash,
@@ -16,21 +16,6 @@ function assertNever(value: never): never {
16
16
  )
17
17
  }
18
18
 
19
- const LIGHTER_ACCOUNT_TYPE_STANDARD = 0
20
- const LIGHTER_ACCOUNT_TYPE_PREMIUM = 1
21
-
22
- /**
23
- * Tier decode for the unauthenticated read, where `/accountLimits` — and with
24
- * it the tier string — is out of reach. Neither official Lighter client
25
- * documents an integer for any tier, so `0` (the `account_type` every sampled
26
- * public account reports) reads as the default tier `standard`, and `1` as
27
- * `premium`. No integer is known for `plus`: that account projects `null` here.
28
- */
29
- const ACCOUNT_TYPE_INT_TO_WIRE: Readonly<Record<number, string>> = {
30
- [LIGHTER_ACCOUNT_TYPE_STANDARD]: 'standard',
31
- [LIGHTER_ACCOUNT_TYPE_PREMIUM]: 'premium',
32
- }
33
-
34
19
  // Wire strings match the backend descriptor's ParamOption values. An unmapped
35
20
  // int projects to null.
36
21
  const ACCOUNT_MODE_INT_TO_WIRE: Readonly<Record<number, string>> = {
@@ -43,8 +28,10 @@ const ACCOUNT_MODE_INT_TO_WIRE: Readonly<Record<number, string>> = {
43
28
  * `userTierName` decides it, but only when the descriptor enumerates the
44
29
  * string: Lighter owns that vocabulary, so an unrecognised value projects
45
30
  * `null` instead of a mis-reported tier. A descriptor whose parameter carries
46
- * no `values` array enumerates nothing, so it also projects `null`. With no
47
- * tier string in hand the integer map decides.
31
+ * no `values` array enumerates nothing, so it also projects `null`. The
32
+ * unauthenticated read reaches no `/accountLimits` and so carries no tier
33
+ * string, which projects `null` as well: `config.accountType` is Lighter's
34
+ * `SubAccountType`, not a tier, so nothing else in the config decides it.
48
35
  */
49
36
  function resolveAccountTier(
50
37
  descriptor: ProviderAction,
@@ -52,7 +39,7 @@ function resolveAccountTier(
52
39
  ): string | null {
53
40
  const { userTierName } = config
54
41
  if (userTierName === undefined) {
55
- return ACCOUNT_TYPE_INT_TO_WIRE[config.accountType] ?? null
42
+ return null
56
43
  }
57
44
  const enumerated = descriptor.params?.[0]?.values ?? []
58
45
  return enumerated.some((option) => option.value === userTierName)
package/src/constants.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExplorerChainId } from '@lifi/perps-sdk'
1
2
  import type { FeeTier, LighterProviderKey } from '@lifi/perps-types'
2
3
  import { LT_ASSET_ID_USDC } from './types/action.js'
3
4
 
@@ -114,6 +115,11 @@ export interface LighterDeployment {
114
115
  * When omitted, transfer links are not emitted for this deployment.
115
116
  */
116
117
  explorerTxBaseUrl?: string
118
+ /**
119
+ * L1 the deployment's bridge settles on. Deposit and withdrawal rows report
120
+ * an `l1_tx_hash` on this chain, not on the zkLighter L2.
121
+ */
122
+ bridgeChainId: ExplorerChainId
117
123
  }
118
124
 
119
125
  /**
@@ -128,6 +134,7 @@ export const LIGHTER_MAINNET_DEPLOYMENT: LighterDeployment = Object.freeze({
128
134
  signerChainId: LIGHTER_MAINNET_SIGNER_CHAIN_ID,
129
135
  collateral: LIGHTER_COLLATERAL_ASSETS[LIGHTER_PROVIDER_KEY],
130
136
  explorerTxBaseUrl: DEFAULT_LIGHTER_EXPLORER_TX_BASE_URL,
137
+ bridgeChainId: ExplorerChainId.ETHEREUM,
131
138
  })
132
139
 
133
140
  /**
@@ -155,9 +162,9 @@ export const LIGHTER_RH_WS_URL = 'wss://api.rh.lighter.xyz/stream'
155
162
  export const LIGHTER_RH_SIGNER_CHAIN_ID = 466324
156
163
 
157
164
  /**
158
- * Lighter on Robinhood chain, as served by `lighterRhProvider()`. No explorer
159
- * base URL: the RH zkLighter explorer is unpublished, so transfer links are
160
- * left unset rather than pointed at the mainnet explorer.
165
+ * Lighter on Robinhood chain, as served by `lighterRhProvider()`. No L2
166
+ * explorer base URL: the RH zkLighter explorer is unpublished, so transfer
167
+ * links are left unset rather than pointed at the mainnet explorer.
161
168
  *
162
169
  * @public
163
170
  */
@@ -168,6 +175,7 @@ export const LIGHTER_RH_DEPLOYMENT: LighterDeployment = Object.freeze({
168
175
  signerChainId: LIGHTER_RH_SIGNER_CHAIN_ID,
169
176
  collateral: LIGHTER_COLLATERAL_ASSETS[LIGHTER_RH_PROVIDER_KEY],
170
177
  explorerTxBaseUrl: undefined,
178
+ bridgeChainId: ExplorerChainId.ROBINHOOD,
171
179
  })
172
180
 
173
181
  /** @internal */
@@ -109,6 +109,7 @@ const NIL_INTEGRATOR_MAKER_FEE = 0
109
109
  const SELF_TRADE_BEHAVIOR_EXPIRE_MAKER = 0
110
110
  const SELF_TRADE_EQUALITY_ACCOUNT_INDEX = 0
111
111
  const SKIP_NONCE_DISABLED = 0
112
+ const NIL_ORDER_VERSION = 0
112
113
  // CancelAll across every market (lighter-go `NilMarketIndex`); a real index
113
114
  // scopes the cancel to a single market.
114
115
  const NIL_MARKET_INDEX = 255
@@ -552,6 +553,7 @@ export class LighterSigner {
552
553
  SELF_TRADE_EQUALITY_ACCOUNT_INDEX,
553
554
  SKIP_NONCE_DISABLED,
554
555
  nonce,
556
+ NIL_ORDER_VERSION,
555
557
  ctx.apiKeyIndex,
556
558
  ctx.accountIndex
557
559
  )
@@ -1,9 +1,14 @@
1
1
  // Lighter submit/execute-path wire shapes (sendTx, fastwithdraw, changeAccountTier).
2
2
 
3
3
  /**
4
- * Request body for Lighter's `/api/v1/sendTx` endpoint. `tx_type` and
5
- * `tx_info` are the signed WASM transaction envelope; `price_protection` is an
6
- * optional venue-side execution guard.
4
+ * Request body for Lighter's `/api/v1/sendTx` endpoint (spec schema
5
+ * `ReqSendTx`). `tx_type` and `tx_info` are the signed WASM transaction
6
+ * envelope. `price_protection` is a venue-side execution guard the spec
7
+ * defaults to `true`, so an absent member leaves the guard on.
8
+ *
9
+ * The generated client carries the same members as `SendTxRequest` in
10
+ * `zklighter-perps/apis/TransactionApi` — the call signature of
11
+ * `TransactionApi.sendTx`, not a `models/` wire model.
7
12
  *
8
13
  * @public
9
14
  */
@@ -28,9 +33,14 @@ export interface LtSendTxResponse {
28
33
  }
29
34
 
30
35
  /**
31
- * Batch request body for Lighter's transaction submission endpoint.
32
- * `tx_types` is a JSON-encoded number array and `tx_infos` a JSON-encoded
33
- * string array; corresponding entries are submitted in array order.
36
+ * Batch request body for Lighter's `/api/v1/sendTxBatch` endpoint (spec schema
37
+ * `ReqSendTxBatch`). `tx_types` is a JSON-encoded number array and `tx_infos` a
38
+ * JSON-encoded string array; corresponding entries are submitted in array
39
+ * order.
40
+ *
41
+ * The generated client carries the same members as `SendTxBatchRequest` in
42
+ * `zklighter-perps/apis/TransactionApi` — the call signature of
43
+ * `TransactionApi.sendTxBatch`, not a `models/` wire model.
34
44
  *
35
45
  * @public
36
46
  */
package/src/types/ws.ts CHANGED
@@ -65,7 +65,7 @@ export type LtWsMarketStatsAllMessage = LtWsMessage & {
65
65
 
66
66
  /**
67
67
  * Spot market stats carry no funding/open-interest fields and address
68
- * markets by their spot `market_id` (2048+).
68
+ * markets by their `market_id`.
69
69
  * @public
70
70
  */
71
71
  export type LtWsSpotMarketStats = {
@@ -1,18 +1,13 @@
1
1
  import { PerpsError } from '@lifi/perps-sdk'
2
- import { type ActivityItem, PerpsErrorCode } from '@lifi/perps-types'
2
+ import {
3
+ type ActivityItem,
4
+ ActivityType,
5
+ PerpsErrorCode,
6
+ } from '@lifi/perps-types'
3
7
 
4
8
  /**
5
- * Per-endpoint cursor envelope for Lighter activity pagination.
6
- *
7
- * Each key holds the upstream `cursor` / `next_cursor` returned by that
8
- * endpoint on the previous call. When a key is **present** with a non-empty
9
- * string the corresponding endpoint will be re-fetched at that cursor on the
10
- * next call; when a key is **absent** the endpoint is treated as exhausted
11
- * (or never paged because the type filter excluded it) and skipped.
12
- *
13
- * The envelope is round-tripped through `cursor` as base64url JSON so callers
14
- * don't need to know the per-endpoint shape. The encoding mirrors the LI.FI
15
- * backend's wire format so cursors created by the backend remain usable.
9
+ * Endpoint-keyed activity cursor; absent endpoints are exhausted or excluded.
10
+ * Nonempty overflow requires a version-2 base64url JSON envelope.
16
11
  * @public
17
12
  */
18
13
  export interface LighterActivityCursor {
@@ -66,7 +61,7 @@ export const encodeActivityCursor = (
66
61
  if (Object.keys(compact).length === 0) {
67
62
  return undefined
68
63
  }
69
- return toBase64Url(JSON.stringify(compact))
64
+ return toBase64Url(JSON.stringify({ version: 2, ...compact }))
70
65
  }
71
66
 
72
67
  /**
@@ -120,20 +115,57 @@ export const decodeActivityCursor = (
120
115
  'Invalid Lighter activity cursor: overflow must be an array'
121
116
  )
122
117
  }
123
- env.overflow = overflow as ActivityItem[]
118
+ if (overflow.length > 0 && cursorRecord.version !== 2) {
119
+ throw new PerpsError(
120
+ PerpsErrorCode.ValidationError,
121
+ 'Invalid Lighter activity cursor: legacy overflow format; restart pagination'
122
+ )
123
+ }
124
+ env.overflow = overflow.map(toOverflowItem)
124
125
  }
125
126
  return env
126
127
  }
127
128
 
129
+ const ACTIVITY_TYPES: readonly string[] = Object.values(ActivityType)
130
+
131
+ /**
132
+ * A cursor travels through consumer storage, so an overflow row may arrive
133
+ * corrupted or from an incompatible build. Reject it here rather than let a
134
+ * malformed row reach a consumer as a typed activity.
135
+ */
136
+ const toOverflowItem = (row: unknown, index: number): ActivityItem => {
137
+ const invalid = (detail: string): PerpsError =>
138
+ new PerpsError(
139
+ PerpsErrorCode.ValidationError,
140
+ `Invalid Lighter activity cursor: overflow[${index}] ${detail}`
141
+ )
142
+ if (row === null || typeof row !== 'object' || Array.isArray(row)) {
143
+ throw invalid('must be an object')
144
+ }
145
+ const item = row as Record<string, unknown>
146
+ for (const field of ['id', 'provider', 'timestamp'] as const) {
147
+ if (typeof item[field] !== 'string') {
148
+ throw invalid(`${field} must be a string`)
149
+ }
150
+ }
151
+ if (typeof item.type !== 'string' || !ACTIVITY_TYPES.includes(item.type)) {
152
+ throw invalid('carries an unknown activity type')
153
+ }
154
+ return row as ActivityItem
155
+ }
156
+
128
157
  const toBase64Url = (s: string): string => {
129
- const utf8 = unescape(encodeURIComponent(s))
130
- return btoa(utf8).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
158
+ let binary = ''
159
+ for (const byte of new TextEncoder().encode(s)) {
160
+ binary += String.fromCharCode(byte)
161
+ }
162
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
131
163
  }
132
164
 
133
165
  const fromBase64Url = (s: string): string => {
134
166
  const padded = s.replace(/-/g, '+').replace(/_/g, '/') + padFor(s)
135
- const decoded = atob(padded)
136
- return decodeURIComponent(escape(decoded))
167
+ const bytes = Uint8Array.from(atob(padded), (char) => char.charCodeAt(0))
168
+ return new TextDecoder().decode(bytes)
137
169
  }
138
170
 
139
171
  const padFor = (s: string): string => {
@@ -16,15 +16,7 @@ export { estimateLiquidationPrice } from './liquidation.js'
16
16
  export { mapFill } from './mapFill.js'
17
17
  export { mapMarketContext } from './mapMarketContext.js'
18
18
  export { mapOpenPositions } from './mapOpenPositions.js'
19
- export {
20
- classifyAndMapOrders,
21
- isTriggerOrder,
22
- isTriggerType,
23
- mapOrder,
24
- mapOrderDetail,
25
- mapStatusReason,
26
- mapTriggerOrder,
27
- } from './mapOrder.js'
19
+ export { mapOrder, mapOrderUpdates } from './mapOrder.js'
28
20
  export { mapPortfolioHistory } from './mapPortfolioHistory.js'
29
21
  export { leverageFromImf, mapPosition } from './mapPosition.js'
30
22
  export { mapMarketOrderLimits } from './marketLimits.js'
@@ -4,12 +4,7 @@ import {
4
4
  explorerTxUrl,
5
5
  } from '@lifi/perps-sdk'
6
6
  import type { Fill, MarketDisplay } from '@lifi/perps-types'
7
- import {
8
- FillStatus,
9
- LiquidityRole,
10
- OrderSide,
11
- OrderType,
12
- } from '@lifi/perps-types'
7
+ import { LiquidityRole, OrderSide, OrderType } from '@lifi/perps-types'
13
8
  import Big from 'big.js'
14
9
  import {
15
10
  LIGHTER_FEE_TICK_SCALE,
@@ -17,6 +12,7 @@ import {
17
12
  } from '../constants.js'
18
13
  import type { LtTrade } from '../types/index.js'
19
14
  import { leverageFromImf } from './mapPosition.js'
15
+ import { isPlaceholderTxHash } from './txHash.js'
20
16
 
21
17
  /**
22
18
  * Fee charged on a fill, in the market's quote asset. Lighter publishes the
@@ -131,7 +127,6 @@ export const mapFill = (
131
127
  type: OrderType.LIMIT,
132
128
  size: trade.size,
133
129
  price: trade.price,
134
- status: FillStatus.FILLED,
135
130
  liquidity: isMaker ? LiquidityRole.MAKER : LiquidityRole.TAKER,
136
131
  // Lighter charges the fill fee in the market's quote asset.
137
132
  fee:
@@ -162,6 +157,8 @@ export const mapFill = (
162
157
  trade.size
163
158
  ),
164
159
  createdAt: new Date(trade.timestamp).toISOString(),
165
- explorerLink: explorerTxUrl(ExplorerChainId.LIGHTER, trade.tx_hash),
160
+ explorerLink: isPlaceholderTxHash(trade.tx_hash)
161
+ ? undefined
162
+ : explorerTxUrl(ExplorerChainId.LIGHTER, trade.tx_hash),
166
163
  }
167
164
  }