@lifi/perps-sdk-provider-lighter 2.0.0 → 2.2.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 +1 -0
- package/dist/cjs/LighterProvider.d.ts.map +1 -1
- package/dist/cjs/LighterProvider.js +25 -8
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/accountConfig.d.ts.map +1 -1
- package/dist/cjs/accountConfig.js +5 -0
- package/dist/cjs/accountConfig.js.map +1 -1
- package/dist/cjs/accountSummary.js +1 -1
- package/dist/cjs/accountSummary.js.map +1 -1
- package/dist/cjs/types/ws.d.ts +7 -0
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/websocket/LighterWsProvider.d.ts +1 -0
- package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/LighterWsProvider.js +41 -0
- package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
- package/dist/esm/LighterProvider.d.ts +7 -0
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +30 -8
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/accountConfig.d.ts.map +1 -1
- package/dist/esm/accountConfig.js +11 -2
- package/dist/esm/accountConfig.js.map +1 -1
- package/dist/esm/accountSummary.d.ts +3 -3
- package/dist/esm/accountSummary.js +4 -4
- package/dist/esm/accountSummary.js.map +1 -1
- package/dist/esm/types/ws.d.ts +11 -1
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/websocket/LighterWsProvider.d.ts +1 -0
- package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/LighterWsProvider.js +44 -0
- package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
- package/dist/types/LighterProvider.d.ts +7 -0
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/accountConfig.d.ts.map +1 -1
- package/dist/types/accountSummary.d.ts +3 -3
- package/dist/types/types/ws.d.ts +11 -1
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/websocket/LighterWsProvider.d.ts +1 -0
- package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/LighterProvider.ts +77 -22
- package/src/accountConfig.ts +12 -2
- package/src/accountSummary.ts +4 -4
- package/src/types/ws.ts +12 -1
- package/src/websocket/LighterWsProvider.ts +49 -0
package/src/LighterProvider.ts
CHANGED
|
@@ -202,6 +202,13 @@ export interface LighterProviderOptions {
|
|
|
202
202
|
tokenLifetimeSeconds?: number
|
|
203
203
|
/** Re-create when the cached standard token's remaining life is below this. Default 60s. */
|
|
204
204
|
tokenRenewBufferSeconds?: number
|
|
205
|
+
/**
|
|
206
|
+
* LI.FI's Lighter referral code — the code `getAccount` compares the account's
|
|
207
|
+
* applied referral against to populate `LighterAccountConfig.referralPresent`.
|
|
208
|
+
* Must match the backend's configured `LIGHTER_REFERRAL_CODE`; when omitted,
|
|
209
|
+
* `referralPresent` is always `false` (the SDK has nothing to compare against).
|
|
210
|
+
*/
|
|
211
|
+
referralCode?: string
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
interface CachedStandardToken {
|
|
@@ -283,6 +290,7 @@ export const lighterProvider = (
|
|
|
283
290
|
})
|
|
284
291
|
const tokenLifetimeSeconds = options.tokenLifetimeSeconds ?? 60 * 60
|
|
285
292
|
const tokenRenewBufferSeconds = options.tokenRenewBufferSeconds ?? 60
|
|
293
|
+
const referralCode = options.referralCode
|
|
286
294
|
const standardTokenByAddress: Map<string, CachedStandardToken> = new Map()
|
|
287
295
|
// Single-flight + backoff for lazy read-only token creation. Without these,
|
|
288
296
|
// concurrent reads on first load all race `tokens/create`, and any sustained
|
|
@@ -504,6 +512,22 @@ export const lighterProvider = (
|
|
|
504
512
|
account_index: accountIndex,
|
|
505
513
|
})
|
|
506
514
|
|
|
515
|
+
// `used_code` is the referral currently applied to the account (empty string
|
|
516
|
+
// when none). Keyed by L1 address, mirroring Lighter's `/referral/use` write
|
|
517
|
+
// contract.
|
|
518
|
+
const fetchAppliedReferralCode = async (
|
|
519
|
+
client: LighterApiClient,
|
|
520
|
+
l1Address: Address,
|
|
521
|
+
authToken: string
|
|
522
|
+
): Promise<string> => {
|
|
523
|
+
const { used_code } = await client.getAuthed<{ used_code: string }>(
|
|
524
|
+
'/api/v1/referral/userReferrals',
|
|
525
|
+
authToken,
|
|
526
|
+
{ l1_address: l1Address.toLowerCase() }
|
|
527
|
+
)
|
|
528
|
+
return used_code
|
|
529
|
+
}
|
|
530
|
+
|
|
507
531
|
const fetchActiveOrdersForMarket = (
|
|
508
532
|
client: LighterApiClient,
|
|
509
533
|
authToken: string,
|
|
@@ -639,21 +663,40 @@ export const lighterProvider = (
|
|
|
639
663
|
])
|
|
640
664
|
|
|
641
665
|
const registry = getMarketRegistry(requireClient(), LIGHTER_PROVIDER_KEY)
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
666
|
+
const assetRegistry = getAssetRegistry(
|
|
667
|
+
requireClient(),
|
|
668
|
+
LIGHTER_PROVIDER_KEY
|
|
669
|
+
)
|
|
670
|
+
const [
|
|
671
|
+
markets,
|
|
672
|
+
,
|
|
673
|
+
registeredKey,
|
|
674
|
+
limitsResult,
|
|
675
|
+
localKey,
|
|
676
|
+
storedReadOnlyToken,
|
|
677
|
+
appliedReferralCode,
|
|
678
|
+
] = await Promise.all([
|
|
679
|
+
registry.sync(),
|
|
680
|
+
assetRegistry.sync(),
|
|
681
|
+
fetchRegisteredApiKey(client, account.index, DEFAULT_API_KEY_INDEX),
|
|
682
|
+
// No token is a legitimate unauthenticated read → undefined → zero fee
|
|
683
|
+
// tier. A fetch error is NOT: it must propagate, never be coerced to a
|
|
684
|
+
// fabricated 0%/0% fee tier.
|
|
685
|
+
token === undefined
|
|
686
|
+
? Promise.resolve(undefined)
|
|
687
|
+
: retryOnRevoked(opts, params.address, token, (t) =>
|
|
688
|
+
fetchAccountLimits(client, account.index, t)
|
|
689
|
+
),
|
|
690
|
+
keyStore ? keyStore.get(params.address) : Promise.resolve(null),
|
|
691
|
+
readOnlyTokenManager.get(params.address, account.index),
|
|
692
|
+
// No referral code to compare against, or no token to authenticate the
|
|
693
|
+
// read → undefined → `referralPresent: false`.
|
|
694
|
+
!referralCode || token === undefined
|
|
695
|
+
? Promise.resolve(undefined)
|
|
696
|
+
: retryOnRevoked(opts, params.address, token, (t) =>
|
|
697
|
+
fetchAppliedReferralCode(client, params.address, t)
|
|
698
|
+
),
|
|
699
|
+
])
|
|
657
700
|
|
|
658
701
|
// REGISTER_API_KEY is satisfied only when the locally-held keypair
|
|
659
702
|
// matches the key registered on-chain at this slot — existence alone is
|
|
@@ -677,6 +720,10 @@ export const lighterProvider = (
|
|
|
677
720
|
0
|
|
678
721
|
)
|
|
679
722
|
|
|
723
|
+
const quoteAssetByCategory = new Map(
|
|
724
|
+
markets.map((m) => [m.categoryId, m.quoteAsset])
|
|
725
|
+
)
|
|
726
|
+
|
|
680
727
|
// USDC collateral is the category quote asset → collateralBalances.
|
|
681
728
|
// `available_balance` is the free collateral (Lighter's `collateral` is
|
|
682
729
|
// gross, i.e. includes margin locked in positions); the locked portion is
|
|
@@ -684,19 +731,24 @@ export const lighterProvider = (
|
|
|
684
731
|
const collateralBalances: Balance[] = [
|
|
685
732
|
{
|
|
686
733
|
categoryId: LIGHTER_PROVIDER_KEY,
|
|
687
|
-
asset:
|
|
734
|
+
asset:
|
|
735
|
+
quoteAssetByCategory.get(LIGHTER_PROVIDER_KEY) ??
|
|
736
|
+
lighterAsset('USDC', 'USDC'),
|
|
688
737
|
units: account.available_balance,
|
|
689
738
|
valueUsd: account.available_balance,
|
|
690
739
|
},
|
|
691
740
|
]
|
|
692
741
|
// Spot token holdings — non-collateral. USDC value is 1:1; other tokens
|
|
693
742
|
// have no price source at this boundary, so their USD value is unknown.
|
|
694
|
-
const balances: Balance[] = account.assets.map((a) =>
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
743
|
+
const balances: Balance[] = account.assets.map((a) => {
|
|
744
|
+
const assetId = String(a.asset_id)
|
|
745
|
+
return {
|
|
746
|
+
categoryId: LIGHTER_PROVIDER_KEY,
|
|
747
|
+
asset: assetRegistry.get(assetId) ?? lighterAsset(assetId, a.symbol),
|
|
748
|
+
units: a.balance,
|
|
749
|
+
valueUsd: a.symbol === 'USDC' ? a.balance : '0',
|
|
750
|
+
}
|
|
751
|
+
})
|
|
700
752
|
|
|
701
753
|
const assetCollateral = account.assets.flatMap((a) =>
|
|
702
754
|
a.margin_mode === undefined
|
|
@@ -722,6 +774,9 @@ export const lighterProvider = (
|
|
|
722
774
|
readOnlyTokenApproved: storedReadOnlyToken !== undefined,
|
|
723
775
|
readOnlyTokenExpiry: storedReadOnlyToken?.expiry,
|
|
724
776
|
readOnlyTokenScope: storedReadOnlyToken?.scope,
|
|
777
|
+
referralPresent:
|
|
778
|
+
appliedReferralCode !== undefined &&
|
|
779
|
+
appliedReferralCode === referralCode,
|
|
725
780
|
}
|
|
726
781
|
|
|
727
782
|
return {
|
package/src/accountConfig.ts
CHANGED
|
@@ -82,10 +82,20 @@ function projectLighterDescriptor(
|
|
|
82
82
|
satisfied: config.readOnlyTokenApproved,
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// `SET_REFERRAL` is satisfied when LI.FI's referral code is the one applied
|
|
86
|
+
// to the account — an SDK-direct read (`getAccount` → `referralPresent`),
|
|
87
|
+
// since the applied referral is a per-user, auth-gated read. Lighter referral
|
|
88
|
+
// is mutable, so an account on another integrator's code stays gateable.
|
|
89
|
+
case ActionType.SET_REFERRAL:
|
|
90
|
+
return {
|
|
91
|
+
type: descriptor.type,
|
|
92
|
+
values: [],
|
|
93
|
+
satisfied: config.referralPresent,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Backend-gated setup step: `checkSetup` → `createAction` decides
|
|
86
97
|
// satisfaction (the backend emits no actions once satisfied), so the
|
|
87
98
|
// projection carries no local state.
|
|
88
|
-
case ActionType.SET_REFERRAL:
|
|
89
99
|
case ActionType.APPROVE_INTEGRATOR:
|
|
90
100
|
return { type: descriptor.type, values: [] }
|
|
91
101
|
|
package/src/accountSummary.ts
CHANGED
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
/**
|
|
9
9
|
* Roll a Lighter {@link AccountResponse} up into an {@link AccountSummary}.
|
|
10
10
|
* Lighter has a single flat collateral model with no abstraction modes: the
|
|
11
|
-
* collateral rows hold `available_balance
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* collateral rows hold `available_balance`, which nets locked margin out but
|
|
12
|
+
* marks unrealized PnL in — so only the positions' `marginUsed` is added
|
|
13
|
+
* back for the portfolio value, never their PnL.
|
|
14
14
|
*
|
|
15
15
|
* @public
|
|
16
16
|
*/
|
|
@@ -18,5 +18,5 @@ export function getAccountSummary(
|
|
|
18
18
|
account: AccountResponse,
|
|
19
19
|
positions: Position[]
|
|
20
20
|
): AccountSummary {
|
|
21
|
-
return summarizeAccount(account, positions, '
|
|
21
|
+
return summarizeAccount(account, positions, 'net')
|
|
22
22
|
}
|
package/src/types/ws.ts
CHANGED
|
@@ -149,7 +149,18 @@ export type LtWsAccountAllTradesMessage = LtWsMessage & {
|
|
|
149
149
|
data?: { trades?: Record<string, unknown[]> | unknown[] }
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* `user_stats/{account_index}` frame; decimal strings in USD.
|
|
154
|
+
* @public
|
|
155
|
+
*/
|
|
156
|
+
export type LtWsUserStatsMessage = LtWsMessage & {
|
|
157
|
+
stats?: {
|
|
158
|
+
collateral?: string
|
|
159
|
+
portfolio_value?: string
|
|
160
|
+
available_balance?: string
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
153
164
|
export type LtWsAccountAllPositionsMessage = LtWsMessage & {
|
|
154
165
|
type: 'subscribed/account_all_positions' | 'update/account_all_positions'
|
|
155
166
|
positions?: Record<string, unknown>
|
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
LtWsSpotMarketStats,
|
|
41
41
|
LtWsSpotMarketStatsAllMessage,
|
|
42
42
|
LtWsTradeMessage,
|
|
43
|
+
LtWsUserStatsMessage,
|
|
43
44
|
} from '../types/index.js'
|
|
44
45
|
import { LIGHTER_RETRY_DEFAULTS, LighterApiClient } from '../utils/apiClient.js'
|
|
45
46
|
import {
|
|
@@ -310,6 +311,8 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
310
311
|
return `fills:${sub.address.toLowerCase()}`
|
|
311
312
|
case 'positions':
|
|
312
313
|
return `positions:${sub.address.toLowerCase()}`
|
|
314
|
+
case 'accountSummary':
|
|
315
|
+
return `accountSummary:${sub.address.toLowerCase()}`
|
|
313
316
|
// No live wire sub (openChannel returns a no-op), but a stable key is
|
|
314
317
|
// still needed so the base's fan-out registry can track/release it.
|
|
315
318
|
case 'candle':
|
|
@@ -369,6 +372,17 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
369
372
|
}
|
|
370
373
|
return [{ channel: `trade/${id}`, needsAuth: false }]
|
|
371
374
|
}
|
|
375
|
+
if (sub.channel === 'accountSummary') {
|
|
376
|
+
const accountIndex = await this.resolveAccountIndex(sub.address)
|
|
377
|
+
// `user_stats` is publicly readable; no token needed.
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
channel: `user_stats/${accountIndex}`,
|
|
381
|
+
needsAuth: false,
|
|
382
|
+
address: sub.address,
|
|
383
|
+
},
|
|
384
|
+
]
|
|
385
|
+
}
|
|
372
386
|
if (
|
|
373
387
|
sub.channel === 'orderUpdates' ||
|
|
374
388
|
sub.channel === 'fills' ||
|
|
@@ -503,6 +517,14 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
503
517
|
return
|
|
504
518
|
}
|
|
505
519
|
|
|
520
|
+
if (
|
|
521
|
+
msg.type === 'subscribed/user_stats' ||
|
|
522
|
+
msg.type === 'update/user_stats'
|
|
523
|
+
) {
|
|
524
|
+
this.handleUserStats(msg as LtWsUserStatsMessage)
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
506
528
|
if (
|
|
507
529
|
msg.type === 'subscribed/account_all_positions' ||
|
|
508
530
|
msg.type === 'update/account_all_positions'
|
|
@@ -531,6 +553,33 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
|
|
|
531
553
|
})
|
|
532
554
|
}
|
|
533
555
|
|
|
556
|
+
private handleUserStats(msg: LtWsUserStatsMessage): void {
|
|
557
|
+
const address = this.addressFromChannel(msg.channel, 'user_stats')
|
|
558
|
+
if (!address) {
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
const stats = msg.stats
|
|
562
|
+
const portfolio = Number.parseFloat(stats?.portfolio_value ?? '')
|
|
563
|
+
const available = Number.parseFloat(stats?.available_balance ?? '')
|
|
564
|
+
if (!Number.isFinite(portfolio) || !Number.isFinite(available)) {
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
// Identities of the REST 'net' semantics: `available_balance` nets locked
|
|
568
|
+
// margin out with PnL marked in, and `collateral` is gross, PnL-exclusive.
|
|
569
|
+
const collateral = Number.parseFloat(stats?.collateral ?? '')
|
|
570
|
+
this.emit(`accountSummary:${address}`, {
|
|
571
|
+
channel: 'accountSummary',
|
|
572
|
+
data: {
|
|
573
|
+
portfolioValue: portfolio.toString(),
|
|
574
|
+
availableMargin: available.toString(),
|
|
575
|
+
marginUsed: Math.max(portfolio - available, 0).toString(),
|
|
576
|
+
unrealizedPnl: Number.isFinite(collateral)
|
|
577
|
+
? (portfolio - collateral).toString()
|
|
578
|
+
: '0',
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
|
|
534
583
|
private handleAccountTrades(msg: LtWsAccountAllTradesMessage): void {
|
|
535
584
|
const address = this.addressFromChannel(msg.channel, 'account_all_trades')
|
|
536
585
|
if (!address) {
|