@lifi/perps-sdk-provider-lighter 31.0.1 → 33.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 +27 -49
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/signers/LighterSigner.js +1 -1
- package/dist/cjs/signers/LighterSigner.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -1
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +3 -3
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/liquidation.js +1 -1
- package/dist/cjs/utils/liquidation.js.map +1 -1
- package/dist/cjs/utils/mapMarketContext.d.ts.map +1 -1
- package/dist/cjs/utils/mapMarketContext.js +12 -4
- package/dist/cjs/utils/mapMarketContext.js.map +1 -1
- package/dist/cjs/utils/orderFormatting.d.ts +3 -3
- package/dist/cjs/utils/orderFormatting.d.ts.map +1 -1
- package/dist/cjs/utils/orderFormatting.js +5 -5
- package/dist/cjs/utils/orderFormatting.js.map +1 -1
- package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/cjs/utils/withdrawableBalances.js +16 -5
- package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
- package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/LighterWsProvider.js +6 -8
- package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
- package/dist/esm/LighterProvider.d.ts +21 -6
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +33 -54
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/constants.d.ts +4 -2
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +4 -2
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/signers/LighterSigner.js +2 -2
- package/dist/esm/signers/LighterSigner.js.map +1 -1
- package/dist/esm/utils/index.d.ts +1 -1
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/liquidation.js +1 -1
- package/dist/esm/utils/liquidation.js.map +1 -1
- package/dist/esm/utils/mapMarketContext.d.ts.map +1 -1
- package/dist/esm/utils/mapMarketContext.js +18 -4
- package/dist/esm/utils/mapMarketContext.js.map +1 -1
- package/dist/esm/utils/orderFormatting.d.ts +11 -11
- package/dist/esm/utils/orderFormatting.d.ts.map +1 -1
- package/dist/esm/utils/orderFormatting.js +12 -12
- package/dist/esm/utils/orderFormatting.js.map +1 -1
- package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/esm/utils/withdrawableBalances.js +17 -6
- package/dist/esm/utils/withdrawableBalances.js.map +1 -1
- package/dist/esm/websocket/LighterWsProvider.d.ts +3 -2
- package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/LighterWsProvider.js +11 -13
- package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
- package/dist/types/LighterProvider.d.ts +21 -6
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/constants.d.ts +4 -2
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -1
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/mapMarketContext.d.ts.map +1 -1
- package/dist/types/utils/orderFormatting.d.ts +11 -11
- package/dist/types/utils/orderFormatting.d.ts.map +1 -1
- package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
- package/dist/types/websocket/LighterWsProvider.d.ts +3 -2
- package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/LighterProvider.ts +74 -72
- package/src/constants.ts +4 -2
- package/src/signers/LighterSigner.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/liquidation.ts +1 -1
- package/src/utils/mapMarketContext.ts +24 -4
- package/src/utils/orderFormatting.ts +22 -12
- package/src/utils/withdrawableBalances.ts +26 -11
- package/src/websocket/LighterWsProvider.ts +11 -14
package/src/LighterProvider.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACTIVE_ORDER_STATUSES,
|
|
3
|
+
calculateTransferable,
|
|
3
4
|
type DepositFlow,
|
|
4
5
|
explorerTxUrl,
|
|
5
6
|
explorerTxUrlFromBase,
|
|
@@ -9,8 +10,6 @@ import {
|
|
|
9
10
|
getProviders,
|
|
10
11
|
isActiveOrderStatus,
|
|
11
12
|
localStorageAdapter,
|
|
12
|
-
maxOf,
|
|
13
|
-
minOf,
|
|
14
13
|
PerpsError,
|
|
15
14
|
type PerpsProviderPlugin,
|
|
16
15
|
type PerpsSDKClient,
|
|
@@ -66,6 +65,7 @@ import type {
|
|
|
66
65
|
import {
|
|
67
66
|
ActionType,
|
|
68
67
|
ActivityType,
|
|
68
|
+
isDecimalString,
|
|
69
69
|
MarginMode,
|
|
70
70
|
PerpsErrorCode,
|
|
71
71
|
} from '@lifi/perps-types'
|
|
@@ -142,8 +142,6 @@ import { isAssetMarginEnabled } from './utils/assetCollateral.js'
|
|
|
142
142
|
import {
|
|
143
143
|
estimateLiquidationPrice,
|
|
144
144
|
fetchDetailedAccount,
|
|
145
|
-
formatOrderPrice,
|
|
146
|
-
formatOrderSize,
|
|
147
145
|
leverageFromImf,
|
|
148
146
|
lighterAsset,
|
|
149
147
|
lighterWithdrawableBalances,
|
|
@@ -152,6 +150,8 @@ import {
|
|
|
152
150
|
mapOrder,
|
|
153
151
|
mapPortfolioHistory,
|
|
154
152
|
positionRemovableMargin,
|
|
153
|
+
snapOrderPrice,
|
|
154
|
+
snapOrderSize,
|
|
155
155
|
toBigOrNull,
|
|
156
156
|
toIsoFromMs,
|
|
157
157
|
toIsoFromSeconds,
|
|
@@ -275,10 +275,26 @@ interface ReadOnlyCreationBackoff {
|
|
|
275
275
|
* 4. Fresh 1h create via this instance's WASM signer + the user's registered
|
|
276
276
|
* API key
|
|
277
277
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
278
|
+
* Status reads report the account's state and do not throw for a missing
|
|
279
|
+
* credential: `getAccount` returns a zero fee tier and
|
|
280
|
+
* `apiKeyRegistered: false` when the wallet has no locally stored API key
|
|
281
|
+
* matching the key registered on Lighter; otherwise it reads the fee tier
|
|
282
|
+
* with the resolved token and throws `Unauthorized` if Lighter rejects it.
|
|
283
|
+
* `accountExists` and `getDepositFlow` need no token.
|
|
284
|
+
*
|
|
285
|
+
* Data reads throw `PerpsError`, one code per fact:
|
|
286
|
+
* - `SetupRequired` when none of these sources yields a token. The SDK
|
|
287
|
+
* sends no venue request. `getOrders`, `getOrder`, `getFills`,
|
|
288
|
+
* `getPortfolioHistory` and `getActivity` need a token.
|
|
289
|
+
* - `Unauthorized` when Lighter rejects the token that was sent. An
|
|
290
|
+
* SDK-owned read-only token that Lighter reports as revoked is replaced
|
|
291
|
+
* and the read retried once first.
|
|
292
|
+
* - `AccountNotFound` when the wallet has no Lighter account. This also
|
|
293
|
+
* applies to `getPositions`, `getWithdrawableBalances` and
|
|
294
|
+
* `getAvailableToTrade`, which need no token. `getAvailableToTrade`
|
|
295
|
+
* reads the fee tier through `getAccount`, so it also throws
|
|
296
|
+
* `Unauthorized` when a matching local key's token is rejected.
|
|
297
|
+
* An account with no rows resolves an empty list.
|
|
282
298
|
*
|
|
283
299
|
* @public
|
|
284
300
|
*/
|
|
@@ -328,8 +344,7 @@ export interface LighterPerpsProvider extends PerpsProviderPlugin {
|
|
|
328
344
|
/**
|
|
329
345
|
* Resolve a Lighter auth token for `address`, following the resolution order
|
|
330
346
|
* documented on {@link LighterProviderOptions} (the per-call override does not
|
|
331
|
-
* apply here). Returns `undefined` when no source can produce a token
|
|
332
|
-
* callers degrade gracefully.
|
|
347
|
+
* apply here). Returns `undefined` when no source can produce a token.
|
|
333
348
|
*/
|
|
334
349
|
resolveAuthToken(address: Address): Promise<string | undefined>
|
|
335
350
|
}
|
|
@@ -454,8 +469,7 @@ export const createLighterProvider = (
|
|
|
454
469
|
* as the credential that authorises read-only token creation, and as
|
|
455
470
|
* the fallback while creation is failing, bounded by the
|
|
456
471
|
* creation-retry backoff.
|
|
457
|
-
* Returns `undefined` when no source can produce a token
|
|
458
|
-
* gracefully.
|
|
472
|
+
* Returns `undefined` when no source can produce a token.
|
|
459
473
|
*/
|
|
460
474
|
const resolveAuthToken = async (
|
|
461
475
|
opts: SDKRequestOptions | undefined,
|
|
@@ -549,6 +563,25 @@ export const createLighterProvider = (
|
|
|
549
563
|
return attempt
|
|
550
564
|
}
|
|
551
565
|
|
|
566
|
+
/** {@link resolveAuthToken}, throwing `SetupRequired` when no source yields a token. */
|
|
567
|
+
const requireAuthToken = async (
|
|
568
|
+
read: string,
|
|
569
|
+
opts: SDKRequestOptions | undefined,
|
|
570
|
+
address: Address,
|
|
571
|
+
knownApiKey?: LighterApiKey
|
|
572
|
+
): Promise<string> => {
|
|
573
|
+
const token = await resolveAuthToken(opts, address, knownApiKey)
|
|
574
|
+
if (token === undefined) {
|
|
575
|
+
throw new PerpsError(
|
|
576
|
+
PerpsErrorCode.SetupRequired,
|
|
577
|
+
`Lighter ${read} requires an auth token. Pass \`authToken\` to ` +
|
|
578
|
+
'lighterProvider, register an API key + signer for on-demand creation, or ' +
|
|
579
|
+
'forward `options.lighterAuthToken` on the call.'
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
return token
|
|
583
|
+
}
|
|
584
|
+
|
|
552
585
|
const replaceRevokedReadOnlyToken = async (
|
|
553
586
|
address: Address,
|
|
554
587
|
revokedToken: string
|
|
@@ -945,6 +978,7 @@ export const createLighterProvider = (
|
|
|
945
978
|
localKey !== null &&
|
|
946
979
|
normalizeLighterPublicKey(localKey.apiKeyPublicKey) ===
|
|
947
980
|
normalizeLighterPublicKey(registeredKey.public_key)
|
|
981
|
+
// Status read: a missing or stale credential reports `apiKeyRegistered: false`, not a throw.
|
|
948
982
|
const token = apiKeyRegistered
|
|
949
983
|
? await resolveAuthToken(opts, params.address, localKey)
|
|
950
984
|
: undefined
|
|
@@ -1042,37 +1076,37 @@ export const createLighterProvider = (
|
|
|
1042
1076
|
// account's free margin; every other perps-route asset releases none.
|
|
1043
1077
|
const collateralBalances: Balance[] = marginAssets.map((a) => {
|
|
1044
1078
|
const isSettlement = a.asset_id === collateral.assetIndex
|
|
1079
|
+
const marginBalance = toRequiredBig(
|
|
1080
|
+
a.margin_balance,
|
|
1081
|
+
'margin_balance'
|
|
1082
|
+
).toFixed()
|
|
1045
1083
|
return {
|
|
1046
1084
|
...toBalance(
|
|
1047
1085
|
a,
|
|
1048
1086
|
perpsCategory?.id ?? providerKey,
|
|
1049
1087
|
isSettlement ? settlementAsset : registryAsset(a),
|
|
1050
|
-
a.margin_balance
|
|
1088
|
+
isDecimalString(a.margin_balance) ? a.margin_balance : marginBalance
|
|
1051
1089
|
),
|
|
1052
1090
|
transferable: isSettlement
|
|
1053
|
-
?
|
|
1054
|
-
maxOf(availableBalance, new Big(0)),
|
|
1055
|
-
new Big(a.margin_balance)
|
|
1056
|
-
).toFixed()
|
|
1091
|
+
? calculateTransferable(availableBalance.toFixed(), marginBalance)
|
|
1057
1092
|
: '0',
|
|
1058
1093
|
}
|
|
1059
1094
|
})
|
|
1060
1095
|
const balances: Balance[] = heldAssets.map((a) => {
|
|
1061
|
-
const balance =
|
|
1096
|
+
const balance = toRequiredBig(a.balance, 'balance')
|
|
1062
1097
|
return {
|
|
1063
1098
|
...toBalance(
|
|
1064
1099
|
a,
|
|
1065
1100
|
LIGHTER_SPOT_CATEGORY_ID,
|
|
1066
1101
|
registryAsset(a),
|
|
1067
|
-
a.balance
|
|
1102
|
+
isDecimalString(a.balance) ? a.balance : balance.toFixed()
|
|
1068
1103
|
),
|
|
1069
|
-
transferable:
|
|
1070
|
-
maxOf(
|
|
1071
|
-
balance.minus(toRequiredBig(a.locked_balance, 'locked_balance')),
|
|
1072
|
-
new Big(0)
|
|
1073
|
-
),
|
|
1104
|
+
transferable: calculateTransferable(
|
|
1074
1105
|
balance
|
|
1075
|
-
|
|
1106
|
+
.minus(toRequiredBig(a.locked_balance, 'locked_balance'))
|
|
1107
|
+
.toFixed(),
|
|
1108
|
+
balance.toFixed()
|
|
1109
|
+
),
|
|
1076
1110
|
}
|
|
1077
1111
|
})
|
|
1078
1112
|
|
|
@@ -1149,13 +1183,11 @@ export const createLighterProvider = (
|
|
|
1149
1183
|
params: ProviderGetWithdrawableBalancesParams,
|
|
1150
1184
|
opts?: SDKRequestOptions
|
|
1151
1185
|
): Promise<ProviderWithdrawableBalance[]> {
|
|
1152
|
-
const account = await
|
|
1186
|
+
const account = await fetchDetailedAccount(
|
|
1153
1187
|
apiClient(opts),
|
|
1154
1188
|
params.address
|
|
1155
1189
|
)
|
|
1156
|
-
return account
|
|
1157
|
-
? []
|
|
1158
|
-
: lighterWithdrawableBalances(account, collateral.assetIndex)
|
|
1190
|
+
return lighterWithdrawableBalances(account, collateral.assetIndex)
|
|
1159
1191
|
},
|
|
1160
1192
|
|
|
1161
1193
|
async getPositions(
|
|
@@ -1165,16 +1197,9 @@ export const createLighterProvider = (
|
|
|
1165
1197
|
const client = apiClient(opts)
|
|
1166
1198
|
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
1167
1199
|
const [account] = await Promise.all([
|
|
1168
|
-
|
|
1200
|
+
fetchDetailedAccount(client, params.address),
|
|
1169
1201
|
registry.sync(),
|
|
1170
1202
|
])
|
|
1171
|
-
if (account === undefined) {
|
|
1172
|
-
return {
|
|
1173
|
-
provider: providerKey,
|
|
1174
|
-
positions: [],
|
|
1175
|
-
pagination: { limit: params.limit ?? 0, hasMore: false },
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
1203
|
|
|
1179
1204
|
let positions: Position[] = mapOpenPositions(account.positions, (id) =>
|
|
1180
1205
|
toPerpsMarketDisplay(registry.require(String(id)))
|
|
@@ -1279,10 +1304,7 @@ export const createLighterProvider = (
|
|
|
1279
1304
|
if (statuses.size === 0) {
|
|
1280
1305
|
return empty
|
|
1281
1306
|
}
|
|
1282
|
-
const token = await
|
|
1283
|
-
if (token === undefined) {
|
|
1284
|
-
return empty
|
|
1285
|
-
}
|
|
1307
|
+
const token = await requireAuthToken('orders read', opts, params.address)
|
|
1286
1308
|
|
|
1287
1309
|
const client = apiClient(opts)
|
|
1288
1310
|
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
@@ -1371,15 +1393,7 @@ export const createLighterProvider = (
|
|
|
1371
1393
|
params: ProviderGetOrderParams,
|
|
1372
1394
|
opts?: SDKRequestOptions
|
|
1373
1395
|
): Promise<Order> {
|
|
1374
|
-
const token = await
|
|
1375
|
-
if (token === undefined) {
|
|
1376
|
-
throw new PerpsError(
|
|
1377
|
-
PerpsErrorCode.SDKError,
|
|
1378
|
-
'Lighter order lookup requires an auth token. Pass `authToken` to ' +
|
|
1379
|
-
'lighterProvider, register an API key + signer for on-demand creation, or ' +
|
|
1380
|
-
'forward `options.lighterAuthToken` on the call.'
|
|
1381
|
-
)
|
|
1382
|
-
}
|
|
1396
|
+
const token = await requireAuthToken('order lookup', opts, params.address)
|
|
1383
1397
|
|
|
1384
1398
|
const client = apiClient(opts)
|
|
1385
1399
|
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
@@ -1477,14 +1491,7 @@ export const createLighterProvider = (
|
|
|
1477
1491
|
params: ProviderGetFillsParams,
|
|
1478
1492
|
opts?: SDKRequestOptions
|
|
1479
1493
|
): Promise<FillsResponse> {
|
|
1480
|
-
const token = await
|
|
1481
|
-
if (token === undefined) {
|
|
1482
|
-
return {
|
|
1483
|
-
provider: providerKey,
|
|
1484
|
-
items: [],
|
|
1485
|
-
pagination: { limit: params.limit ?? 0, hasMore: false },
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1494
|
+
const token = await requireAuthToken('fills read', opts, params.address)
|
|
1488
1495
|
|
|
1489
1496
|
const client = apiClient(opts)
|
|
1490
1497
|
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
@@ -1541,14 +1548,12 @@ export const createLighterProvider = (
|
|
|
1541
1548
|
const sdkOwnsToken =
|
|
1542
1549
|
opts?.lighterAuthToken === undefined && authTokenSource === undefined
|
|
1543
1550
|
const apiKey = sdkOwnsToken ? await keyStore.get(params.address) : null
|
|
1544
|
-
const token = await
|
|
1551
|
+
const token = await requireAuthToken(
|
|
1552
|
+
'portfolio history read',
|
|
1545
1553
|
opts,
|
|
1546
1554
|
params.address,
|
|
1547
1555
|
apiKey ?? undefined
|
|
1548
1556
|
)
|
|
1549
|
-
if (token === undefined) {
|
|
1550
|
-
return { range: params.range, points: [] }
|
|
1551
|
-
}
|
|
1552
1557
|
|
|
1553
1558
|
const client = apiClient(opts)
|
|
1554
1559
|
const account = await plugin.getAccount({ address: params.address }, opts)
|
|
@@ -1603,14 +1608,11 @@ export const createLighterProvider = (
|
|
|
1603
1608
|
params: ProviderGetActivityParams,
|
|
1604
1609
|
opts?: SDKRequestOptions
|
|
1605
1610
|
): Promise<ActivitiesResponse> {
|
|
1606
|
-
const token = await
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
pagination: { limit: params.limit ?? 0, hasMore: false },
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1611
|
+
const token = await requireAuthToken(
|
|
1612
|
+
'activity read',
|
|
1613
|
+
opts,
|
|
1614
|
+
params.address
|
|
1615
|
+
)
|
|
1614
1616
|
|
|
1615
1617
|
const inputCursor = decodeActivityCursor(params.cursor)
|
|
1616
1618
|
const client = apiClient(opts)
|
|
@@ -1814,9 +1816,9 @@ export const createLighterProvider = (
|
|
|
1814
1816
|
return getAccountSummary(account, positions)
|
|
1815
1817
|
},
|
|
1816
1818
|
|
|
1817
|
-
|
|
1819
|
+
snapOrderPrice,
|
|
1818
1820
|
|
|
1819
|
-
|
|
1821
|
+
snapOrderSize,
|
|
1820
1822
|
|
|
1821
1823
|
estimateLiquidationPrice,
|
|
1822
1824
|
|
package/src/constants.ts
CHANGED
|
@@ -18,8 +18,10 @@ export const LIGHTER_RH_PROVIDER_KEY = 'lighter-rh'
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Category id for Lighter spot token holdings, matching the backend's Lighter
|
|
21
|
-
* spot asset category.
|
|
22
|
-
*
|
|
21
|
+
* spot asset category. Every Lighter call site, REST and WebSocket, uses this
|
|
22
|
+
* constant for the spot category. The perps category id is not a constant:
|
|
23
|
+
* `LighterProvider.getAccount` reads it from the `/providers` category whose
|
|
24
|
+
* `quoteAsset` is not null.
|
|
23
25
|
*
|
|
24
26
|
* @public
|
|
25
27
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decimalToBaseUnits, PerpsError } from '@lifi/perps-sdk'
|
|
2
2
|
import { ActionType, PerpsErrorCode } from '@lifi/perps-types'
|
|
3
3
|
import Big from 'big.js'
|
|
4
4
|
import { LT_ROUTE_PERP, LT_ROUTE_SPOT } from '../types/action.js'
|
|
@@ -632,7 +632,7 @@ export class LighterSigner {
|
|
|
632
632
|
return wasm.SignWithdraw(
|
|
633
633
|
numberField(p, 'asset_index'),
|
|
634
634
|
routeType,
|
|
635
|
-
|
|
635
|
+
decimalToBaseUnits(amount, numberField(p, 'decimals'), 'truncate'),
|
|
636
636
|
SKIP_NONCE_DISABLED,
|
|
637
637
|
nonce,
|
|
638
638
|
ctx.apiKeyIndex,
|
package/src/utils/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { mapPortfolioHistory } from './mapPortfolioHistory.js'
|
|
|
21
21
|
export { leverageFromImf, mapPosition } from './mapPosition.js'
|
|
22
22
|
export { mapMarketOrderLimits } from './marketLimits.js'
|
|
23
23
|
export { mapInterval } from './ohlcvInterval.js'
|
|
24
|
-
export {
|
|
24
|
+
export { snapOrderPrice, snapOrderSize } from './orderFormatting.js'
|
|
25
25
|
export { toIsoFromMs, toIsoFromSeconds } from './time.js'
|
|
26
26
|
export { positionRemovableMargin } from './transferMargin.js'
|
|
27
27
|
export {
|
package/src/utils/liquidation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { numberToDecimalString } from '@lifi/perps-sdk'
|
|
1
2
|
import type { MarketContext } from '@lifi/perps-types'
|
|
2
3
|
import type { LtWsMarketStats, LtWsSpotMarketStats } from '../types/index.js'
|
|
3
4
|
|
|
@@ -7,6 +8,27 @@ const isPerpStats = (
|
|
|
7
8
|
stats: LtWsMarketStats | LtWsSpotMarketStats
|
|
8
9
|
): stats is LtWsMarketStats => 'mark_price' in stats
|
|
9
10
|
|
|
11
|
+
const isFiniteFigure = (value: number | null | undefined): value is number =>
|
|
12
|
+
typeof value === 'number' && Number.isFinite(value)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The 24h figures of one record, omitted where the frame carries no finite
|
|
16
|
+
* number. The venue publishes them as unvalidated JSON, and `market_stats/all`
|
|
17
|
+
* maps every market in one pass, so a single absent figure must not cost the
|
|
18
|
+
* whole frame.
|
|
19
|
+
*/
|
|
20
|
+
const dailyFigures = (stats: {
|
|
21
|
+
daily_price_change?: number | null
|
|
22
|
+
daily_quote_token_volume?: number | null
|
|
23
|
+
}): Pick<MarketContext, 'priceChange24h' | 'volume24h'> => ({
|
|
24
|
+
...(isFiniteFigure(stats.daily_price_change)
|
|
25
|
+
? { priceChange24h: numberToDecimalString(stats.daily_price_change) }
|
|
26
|
+
: {}),
|
|
27
|
+
...(isFiniteFigure(stats.daily_quote_token_volume)
|
|
28
|
+
? { volume24h: numberToDecimalString(stats.daily_quote_token_volume) }
|
|
29
|
+
: {}),
|
|
30
|
+
})
|
|
31
|
+
|
|
10
32
|
/**
|
|
11
33
|
* Map a Lighter market-stats record to a {@link MarketContext}: `index_price`
|
|
12
34
|
* is the oracle, `mid_price` the mid. Perp records carry the venue
|
|
@@ -24,8 +46,7 @@ export const mapMarketContext = (
|
|
|
24
46
|
midPrice: stats.mid_price,
|
|
25
47
|
markPrice: stats.mark_price,
|
|
26
48
|
oraclePrice: stats.index_price,
|
|
27
|
-
|
|
28
|
-
volume24h: String(stats.daily_quote_token_volume),
|
|
49
|
+
...dailyFigures(stats),
|
|
29
50
|
openInterest: stats.open_interest,
|
|
30
51
|
funding: {
|
|
31
52
|
rate: stats.current_funding_rate,
|
|
@@ -38,7 +59,6 @@ export const mapMarketContext = (
|
|
|
38
59
|
midPrice: stats.mid_price,
|
|
39
60
|
markPrice: stats.mid_price,
|
|
40
61
|
oraclePrice: stats.index_price,
|
|
41
|
-
|
|
42
|
-
volume24h: String(stats.daily_quote_token_volume),
|
|
62
|
+
...dailyFigures(stats),
|
|
43
63
|
}
|
|
44
64
|
}
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lighter order size and price
|
|
2
|
+
* Lighter order size and price snapping for exchange submission.
|
|
3
3
|
*
|
|
4
4
|
* Lighter publishes a flat per-market decimal budget on its order books
|
|
5
5
|
* (`supported_price_decimals` / `supported_size_decimals`), surfaced on the
|
|
6
|
-
* generic {@link Market} as `priceDecimals` / `szDecimals`. Prices round
|
|
7
|
-
* the tick grid; sizes truncate
|
|
6
|
+
* generic {@link Market} as `priceDecimals` / `szDecimals`. Prices round onto
|
|
7
|
+
* the tick grid; sizes truncate onto the lot grid.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { PerpsError } from '@lifi/perps-sdk'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type DecimalString,
|
|
13
|
+
type Market,
|
|
14
|
+
PerpsErrorCode,
|
|
15
|
+
} from '@lifi/perps-types'
|
|
12
16
|
import Big from 'big.js'
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
* `priceDecimals` (Lighter's `supported_price_decimals`), trailing
|
|
17
|
-
* stripped.
|
|
19
|
+
* Snap a price onto a Lighter market's tick grid: rounded half-up to the
|
|
20
|
+
* market's `priceDecimals` (Lighter's `supported_price_decimals`), trailing
|
|
21
|
+
* zeros stripped.
|
|
18
22
|
*
|
|
19
23
|
* @throws {PerpsError} `ValidationError` when `market.priceDecimals` is
|
|
20
24
|
* absent — without the venue's tick grid no correct price can be produced.
|
|
21
25
|
* @public
|
|
22
26
|
*/
|
|
23
|
-
export function
|
|
27
|
+
export function snapOrderPrice(
|
|
28
|
+
market: Market,
|
|
29
|
+
price: DecimalString
|
|
30
|
+
): DecimalString {
|
|
24
31
|
if (market.priceDecimals === undefined) {
|
|
25
32
|
throw new PerpsError(
|
|
26
33
|
PerpsErrorCode.ValidationError,
|
|
27
34
|
`Market '${market.id}' carries no priceDecimals; Lighter order prices ` +
|
|
28
|
-
`cannot be
|
|
35
|
+
`cannot be snapped without the market's tick grid.`
|
|
29
36
|
)
|
|
30
37
|
}
|
|
31
38
|
const rounded = new Big(price).round(market.priceDecimals, Big.roundHalfUp)
|
|
@@ -33,14 +40,17 @@ export function formatOrderPrice(market: Market, price: number): string {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
|
-
*
|
|
37
|
-
* up, so the size cannot exceed the user's balance) to the market's
|
|
43
|
+
* Snap a size onto a Lighter market's lot grid: truncated toward zero (never
|
|
44
|
+
* rounded up, so the size cannot exceed the user's balance) to the market's
|
|
38
45
|
* `szDecimals` (Lighter's `supported_size_decimals`), trailing zeros stripped.
|
|
39
46
|
*
|
|
40
47
|
* @param size - Size in base-asset units as a non-negative magnitude.
|
|
41
48
|
* @public
|
|
42
49
|
*/
|
|
43
|
-
export function
|
|
50
|
+
export function snapOrderSize(
|
|
51
|
+
market: Market,
|
|
52
|
+
size: DecimalString
|
|
53
|
+
): DecimalString {
|
|
44
54
|
const truncated = new Big(size).round(market.szDecimals, Big.roundDown)
|
|
45
55
|
return truncated.eq(0) ? '0' : truncated.toFixed()
|
|
46
56
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
calculateTransferable,
|
|
3
|
+
calculateWithdrawMax,
|
|
4
|
+
type ProviderWithdrawableBalance,
|
|
5
|
+
} from '@lifi/perps-sdk'
|
|
2
6
|
import Big from 'big.js'
|
|
3
7
|
import type { LtAccount } from '../types/account.js'
|
|
4
8
|
import { toRequiredBig } from './decimal.js'
|
|
@@ -29,21 +33,32 @@ export const lighterWithdrawableBalances = (
|
|
|
29
33
|
toRequiredBig(asset.locked_balance, 'locked_balance')
|
|
30
34
|
)
|
|
31
35
|
if (spot.gt(0)) {
|
|
32
|
-
|
|
36
|
+
const available = spot.toFixed()
|
|
37
|
+
rows.push({
|
|
38
|
+
assetId,
|
|
39
|
+
route: 'spot',
|
|
40
|
+
available,
|
|
41
|
+
max: calculateWithdrawMax({ available }),
|
|
42
|
+
})
|
|
33
43
|
}
|
|
34
44
|
const marginBalance = toRequiredBig(asset.margin_balance, 'margin_balance')
|
|
35
45
|
const perps =
|
|
36
46
|
asset.asset_id === settlementAssetIndex
|
|
37
|
-
?
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
),
|
|
42
|
-
marginBalance
|
|
47
|
+
? calculateTransferable(
|
|
48
|
+
toRequiredBig(
|
|
49
|
+
account.available_balance,
|
|
50
|
+
'available_balance'
|
|
51
|
+
).toFixed(),
|
|
52
|
+
marginBalance.toFixed()
|
|
43
53
|
)
|
|
44
|
-
: marginBalance
|
|
45
|
-
if (perps.gt(0)) {
|
|
46
|
-
rows.push({
|
|
54
|
+
: marginBalance.toFixed()
|
|
55
|
+
if (new Big(perps).gt(0)) {
|
|
56
|
+
rows.push({
|
|
57
|
+
assetId,
|
|
58
|
+
route: 'perps',
|
|
59
|
+
available: perps,
|
|
60
|
+
max: calculateWithdrawMax({ available: perps }),
|
|
61
|
+
})
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
return rows
|
|
@@ -293,15 +293,6 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
293
293
|
|
|
294
294
|
const wireChannels = await this.resolveChannel(sub)
|
|
295
295
|
|
|
296
|
-
// Reject before any wire sub registers: a registered sub whose token can
|
|
297
|
-
// never be resolved is retried on every reopen without the caller ever
|
|
298
|
-
// seeing the failure.
|
|
299
|
-
for (const { channel, needsAuth, address } of wireChannels) {
|
|
300
|
-
if (needsAuth) {
|
|
301
|
-
await this.requireAuthToken(channel, address)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
296
|
// Acquired before the wire subs register, so a rejected spot-mark open
|
|
306
297
|
// leaves no `user_stats`/`account_all` entry behind for the next replay.
|
|
307
298
|
const releaseSpotMarks =
|
|
@@ -392,8 +383,9 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
392
383
|
* The auth token a gated channel's subscribe frame carries, or a throw when
|
|
393
384
|
* no resolver is wired or the resolver yields none. Shared by the
|
|
394
385
|
* subscribe-time guard and the per-send resolve so both report one message.
|
|
395
|
-
* Throws {@link PerpsError}
|
|
396
|
-
*
|
|
386
|
+
* Throws {@link PerpsError}: `SetupRequired` when the resolver yields no
|
|
387
|
+
* token, the code the REST reads throw for the same missing credential, and
|
|
388
|
+
* `SDKError` when no resolver is wired, which no account setup can fix.
|
|
397
389
|
*/
|
|
398
390
|
private async requireAuthToken(
|
|
399
391
|
channel: string,
|
|
@@ -402,7 +394,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
402
394
|
const resolve = this.authTokenResolver()
|
|
403
395
|
if (!resolve || !address) {
|
|
404
396
|
throw new PerpsError(
|
|
405
|
-
PerpsErrorCode.
|
|
397
|
+
PerpsErrorCode.SDKError,
|
|
406
398
|
`Lighter WS channel '${channel}' requires authentication but no auth-token resolver was available. ` +
|
|
407
399
|
'Register `lighterProvider()` on the same client, or pass `resolveAuthToken` to `lighterWsProvider`.'
|
|
408
400
|
)
|
|
@@ -410,7 +402,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
410
402
|
const token = await resolve(address)
|
|
411
403
|
if (!token) {
|
|
412
404
|
throw new PerpsError(
|
|
413
|
-
PerpsErrorCode.
|
|
405
|
+
PerpsErrorCode.SetupRequired,
|
|
414
406
|
`Lighter WS channel '${channel}' requires authentication but no token was available for ${address}.`
|
|
415
407
|
)
|
|
416
408
|
}
|
|
@@ -534,13 +526,18 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
534
526
|
sub.channel === 'fills' ||
|
|
535
527
|
sub.channel === 'positions'
|
|
536
528
|
) {
|
|
537
|
-
const accountIndex = await this.resolveAccountIndex(sub.address)
|
|
538
529
|
const lighterChannel = LIGHTER_AUTH_CHANNEL[sub.channel]
|
|
539
530
|
// `account_all_trades` is publicly readable per the Lighter WS spec —
|
|
540
531
|
// an auth token only filters events to the user's own account, which
|
|
541
532
|
// we don't currently use to scope further. Skip the token here so a
|
|
542
533
|
// user without a registered API key still gets their fill stream.
|
|
543
534
|
const needsAuth = sub.channel !== 'fills'
|
|
535
|
+
// Before the account lookup and before registration: a registered sub
|
|
536
|
+
// with no token would fail on every reopen without the caller seeing it.
|
|
537
|
+
if (needsAuth) {
|
|
538
|
+
await this.requireAuthToken(lighterChannel, sub.address)
|
|
539
|
+
}
|
|
540
|
+
const accountIndex = await this.resolveAccountIndex(sub.address)
|
|
544
541
|
return [
|
|
545
542
|
{
|
|
546
543
|
channel: `${lighterChannel}/${accountIndex}`,
|