@lifi/perps-sdk-provider-lighter 17.2.2 → 18.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/README.md +8 -0
- package/dist/cjs/LighterProvider.d.ts.map +1 -1
- package/dist/cjs/LighterProvider.js +76 -57
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/accountConfig.d.ts.map +1 -1
- package/dist/cjs/accountConfig.js.map +1 -1
- package/dist/cjs/constants.d.ts +2 -1
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +3 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts +3 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.js +14 -2
- package/dist/cjs/signers/LighterKeyStore.js.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts +7 -0
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.js +19 -7
- package/dist/cjs/signers/LighterReadOnlyTokenManager.js.map +1 -1
- package/dist/cjs/signers/signActions.d.ts.map +1 -1
- package/dist/cjs/signers/signActions.js +8 -0
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/utils/apiClient.d.ts +12 -0
- package/dist/cjs/utils/apiClient.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.js +62 -7
- package/dist/cjs/utils/apiClient.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -0
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +3 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/marketLimits.d.ts +7 -0
- package/dist/cjs/utils/marketLimits.d.ts.map +1 -0
- package/dist/cjs/utils/marketLimits.js +15 -0
- package/dist/cjs/utils/marketLimits.js.map +1 -0
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +78 -102
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/accountConfig.d.ts.map +1 -1
- package/dist/esm/accountConfig.js +2 -4
- package/dist/esm/accountConfig.js.map +1 -1
- package/dist/esm/constants.d.ts +7 -1
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +7 -1
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/signers/LighterKeyStore.d.ts +4 -1
- package/dist/esm/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/esm/signers/LighterKeyStore.js +15 -3
- package/dist/esm/signers/LighterKeyStore.js.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts +12 -0
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.js +23 -7
- package/dist/esm/signers/LighterReadOnlyTokenManager.js.map +1 -1
- package/dist/esm/signers/signActions.d.ts.map +1 -1
- package/dist/esm/signers/signActions.js +9 -0
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/utils/apiClient.d.ts +34 -7
- package/dist/esm/utils/apiClient.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.js +77 -14
- package/dist/esm/utils/apiClient.js.map +1 -1
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/marketLimits.d.ts +8 -0
- package/dist/esm/utils/marketLimits.d.ts.map +1 -0
- package/dist/esm/utils/marketLimits.js +12 -0
- package/dist/esm/utils/marketLimits.js.map +1 -0
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/accountConfig.d.ts.map +1 -1
- package/dist/types/constants.d.ts +7 -1
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/signers/LighterKeyStore.d.ts +4 -1
- package/dist/types/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts +12 -0
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/utils/apiClient.d.ts +34 -7
- package/dist/types/utils/apiClient.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/marketLimits.d.ts +8 -0
- package/dist/types/utils/marketLimits.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/LighterProvider.ts +103 -138
- package/src/accountConfig.ts +2 -4
- package/src/constants.ts +8 -0
- package/src/signers/LighterKeyStore.ts +28 -3
- package/src/signers/LighterReadOnlyTokenManager.ts +39 -6
- package/src/signers/signActions.ts +10 -0
- package/src/utils/apiClient.ts +106 -13
- package/src/utils/index.ts +1 -0
- package/src/utils/marketLimits.ts +28 -0
package/src/LighterProvider.ts
CHANGED
|
@@ -75,7 +75,10 @@ import {
|
|
|
75
75
|
} from './constants.js'
|
|
76
76
|
import { lighterDepositFlow } from './depositFlow.js'
|
|
77
77
|
import { createAuthToken } from './signers/createAuthToken.js'
|
|
78
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
type LighterApiKey,
|
|
80
|
+
LighterKeyStore,
|
|
81
|
+
} from './signers/LighterKeyStore.js'
|
|
79
82
|
import { LighterReadOnlyTokenManager } from './signers/LighterReadOnlyTokenManager.js'
|
|
80
83
|
import { LighterSigner } from './signers/LighterSigner.js'
|
|
81
84
|
import {
|
|
@@ -103,9 +106,8 @@ import {
|
|
|
103
106
|
import {
|
|
104
107
|
LIGHTER_RETRY_DEFAULTS,
|
|
105
108
|
LighterApiClient,
|
|
106
|
-
|
|
109
|
+
LighterTokenRevokedError,
|
|
107
110
|
} from './utils/apiClient.js'
|
|
108
|
-
import { fetchAppliedReferralCode } from './utils/appliedReferralCode.js'
|
|
109
111
|
import { isAssetMarginEnabled } from './utils/assetCollateral.js'
|
|
110
112
|
import {
|
|
111
113
|
classifyAndMapOrders,
|
|
@@ -311,11 +313,6 @@ export const createLighterProvider = (
|
|
|
311
313
|
})
|
|
312
314
|
const keyStore = new LighterKeyStore(storage, providerKey)
|
|
313
315
|
const apiKeyFreshness = createLighterApiKeyFreshness()
|
|
314
|
-
const readOnlyTokenManager = new LighterReadOnlyTokenManager({
|
|
315
|
-
storage,
|
|
316
|
-
providerKey,
|
|
317
|
-
lighterApiUrl: restUrl,
|
|
318
|
-
})
|
|
319
316
|
const tokenLifetimeSeconds = options.tokenLifetimeSeconds ?? 60 * 60
|
|
320
317
|
const tokenRenewBufferSeconds = options.tokenRenewBufferSeconds ?? 60
|
|
321
318
|
const standardTokenByAddress: Map<string, CachedStandardToken> = new Map()
|
|
@@ -326,6 +323,7 @@ export const createLighterProvider = (
|
|
|
326
323
|
const readOnlyCreationInFlight: Map<string, Promise<string>> = new Map()
|
|
327
324
|
const readOnlyCreationBackoff: Map<string, ReadOnlyCreationBackoff> =
|
|
328
325
|
new Map()
|
|
326
|
+
const rateLimitHold = { untilMs: 0 }
|
|
329
327
|
|
|
330
328
|
const apiClient = (opts?: SDKRequestOptions): LighterApiClient => {
|
|
331
329
|
const client = requireClient()
|
|
@@ -337,9 +335,17 @@ export const createLighterProvider = (
|
|
|
337
335
|
providerKey
|
|
338
336
|
),
|
|
339
337
|
fetchImpl: client.config.fetch,
|
|
338
|
+
rateLimitHold,
|
|
340
339
|
})
|
|
341
340
|
}
|
|
342
341
|
|
|
342
|
+
const readOnlyTokenManager = new LighterReadOnlyTokenManager({
|
|
343
|
+
storage,
|
|
344
|
+
providerKey,
|
|
345
|
+
lighterApiUrl: restUrl,
|
|
346
|
+
fetchImpl: (input, init) => apiClient().request(input, init),
|
|
347
|
+
})
|
|
348
|
+
|
|
343
349
|
const getStandardAuthToken = async (
|
|
344
350
|
address: Address,
|
|
345
351
|
apiKeyPrivateKey: string,
|
|
@@ -385,7 +391,8 @@ export const createLighterProvider = (
|
|
|
385
391
|
*/
|
|
386
392
|
const resolveAuthToken = async (
|
|
387
393
|
opts: SDKRequestOptions | undefined,
|
|
388
|
-
address?: Address
|
|
394
|
+
address?: Address,
|
|
395
|
+
knownApiKey?: LighterApiKey
|
|
389
396
|
): Promise<string | undefined> => {
|
|
390
397
|
if (opts?.lighterAuthToken !== undefined) {
|
|
391
398
|
return opts.lighterAuthToken
|
|
@@ -396,7 +403,7 @@ export const createLighterProvider = (
|
|
|
396
403
|
if (address === undefined) {
|
|
397
404
|
return undefined
|
|
398
405
|
}
|
|
399
|
-
const apiKey = await keyStore.get(address)
|
|
406
|
+
const apiKey = knownApiKey ?? (await keyStore.get(address))
|
|
400
407
|
if (apiKey === null) {
|
|
401
408
|
return undefined
|
|
402
409
|
}
|
|
@@ -468,27 +475,60 @@ export const createLighterProvider = (
|
|
|
468
475
|
return attempt
|
|
469
476
|
}
|
|
470
477
|
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// rides reads during the creation-failure fallback and authorises
|
|
476
|
-
// read-only token creation — so a revocation must re-sign it too.
|
|
477
|
-
standardTokenByAddress.delete(cacheKey)
|
|
478
|
+
const replaceRevokedReadOnlyToken = async (
|
|
479
|
+
address: Address,
|
|
480
|
+
revokedToken: string
|
|
481
|
+
): Promise<string | undefined> => {
|
|
478
482
|
const apiKey = await keyStore.get(address)
|
|
479
|
-
if (apiKey
|
|
480
|
-
|
|
483
|
+
if (apiKey === null) {
|
|
484
|
+
return undefined
|
|
485
|
+
}
|
|
486
|
+
const flightKey = address.toLowerCase()
|
|
487
|
+
const activeFlight = readOnlyCreationInFlight.get(flightKey)
|
|
488
|
+
if (activeFlight !== undefined) {
|
|
489
|
+
return activeFlight
|
|
490
|
+
}
|
|
491
|
+
const current = await readOnlyTokenManager.get(address, apiKey.accountIndex)
|
|
492
|
+
if (current !== undefined && current.token !== revokedToken) {
|
|
493
|
+
return current.token
|
|
494
|
+
}
|
|
495
|
+
const inFlightAfterRead = readOnlyCreationInFlight.get(flightKey)
|
|
496
|
+
if (inFlightAfterRead !== undefined) {
|
|
497
|
+
return inFlightAfterRead
|
|
498
|
+
}
|
|
499
|
+
const attempt = (async (): Promise<string> => {
|
|
500
|
+
const standardToken = await getStandardAuthToken(
|
|
501
|
+
address,
|
|
502
|
+
apiKey.apiKeyPrivateKey,
|
|
503
|
+
{
|
|
504
|
+
apiKeyIndex: apiKey.apiKeyIndex,
|
|
505
|
+
accountIndex: apiKey.accountIndex,
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
const { token } = await readOnlyTokenManager.replaceRevoked(
|
|
509
|
+
standardToken,
|
|
510
|
+
{
|
|
511
|
+
address,
|
|
512
|
+
accountIndex: apiKey.accountIndex,
|
|
513
|
+
expirySeconds:
|
|
514
|
+
Math.floor(Date.now() / 1000) +
|
|
515
|
+
DEFAULT_READ_ONLY_TOKEN_LIFETIME_SECONDS,
|
|
516
|
+
scope: 'all',
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
readOnlyCreationBackoff.delete(flightKey)
|
|
520
|
+
return token.token
|
|
521
|
+
})()
|
|
522
|
+
readOnlyCreationInFlight.set(flightKey, attempt)
|
|
523
|
+
try {
|
|
524
|
+
return await attempt
|
|
525
|
+
} finally {
|
|
526
|
+
if (readOnlyCreationInFlight.get(flightKey) === attempt) {
|
|
527
|
+
readOnlyCreationInFlight.delete(flightKey)
|
|
528
|
+
}
|
|
481
529
|
}
|
|
482
530
|
}
|
|
483
531
|
|
|
484
|
-
/**
|
|
485
|
-
* Run an auth-gated read; if Lighter rejects the token (revoked server-side —
|
|
486
|
-
* invisible to `checkSetup`, since the read-only token is a client-only
|
|
487
|
-
* concern), evict the SDK-resolved credentials (stored read-only token and
|
|
488
|
-
* cached standard token) and retry once with freshly resolved ones. Only
|
|
489
|
-
* self-heals tokens the SDK itself resolved — a caller-supplied
|
|
490
|
-
* `lighterAuthToken`/`authToken` source is the caller's to fix.
|
|
491
|
-
*/
|
|
492
532
|
const retryOnRevoked = async <T>(
|
|
493
533
|
opts: SDKRequestOptions | undefined,
|
|
494
534
|
address: Address,
|
|
@@ -500,11 +540,10 @@ export const createLighterProvider = (
|
|
|
500
540
|
} catch (err) {
|
|
501
541
|
const sdkOwnsToken =
|
|
502
542
|
opts?.lighterAuthToken === undefined && authTokenSource === undefined
|
|
503
|
-
if (!(err instanceof
|
|
543
|
+
if (!(err instanceof LighterTokenRevokedError) || !sdkOwnsToken) {
|
|
504
544
|
throw err
|
|
505
545
|
}
|
|
506
|
-
await
|
|
507
|
-
const fresh = await resolveAuthToken(opts, address)
|
|
546
|
+
const fresh = await replaceRevokedReadOnlyToken(address, token)
|
|
508
547
|
if (fresh === undefined || fresh === token) {
|
|
509
548
|
throw err
|
|
510
549
|
}
|
|
@@ -512,38 +551,6 @@ export const createLighterProvider = (
|
|
|
512
551
|
}
|
|
513
552
|
}
|
|
514
553
|
|
|
515
|
-
/**
|
|
516
|
-
* Degrade an auth-gated `getAccount` read to `undefined` when Lighter
|
|
517
|
-
* rejects an SDK-owned token even after `retryOnRevoked` re-resolved it —
|
|
518
|
-
* the signature of a stale local API key (the venue's slot was
|
|
519
|
-
* re-registered elsewhere). `getAccount` must resolve in that state so
|
|
520
|
-
* `apiKeyRegistered: false` can render the REGISTER_API_KEY gate, which is
|
|
521
|
-
* the only recovery. A caller-supplied token stays the caller's to fix, so
|
|
522
|
-
* its rejection propagates, as does every other error class.
|
|
523
|
-
*/
|
|
524
|
-
const degradeOnAuthRejection = async <T>(
|
|
525
|
-
opts: SDKRequestOptions | undefined,
|
|
526
|
-
endpoint: string,
|
|
527
|
-
read: Promise<T>
|
|
528
|
-
): Promise<T | undefined> => {
|
|
529
|
-
try {
|
|
530
|
-
return await read
|
|
531
|
-
} catch (err) {
|
|
532
|
-
const sdkOwnsToken =
|
|
533
|
-
opts?.lighterAuthToken === undefined && authTokenSource === undefined
|
|
534
|
-
if (err instanceof LighterAuthRejectedError && sdkOwnsToken) {
|
|
535
|
-
console.warn(
|
|
536
|
-
`[lighter] ${endpoint} rejected the auth token after re-resolution; ` +
|
|
537
|
-
'degrading the read — the stored API key no longer matches the ' +
|
|
538
|
-
'registered key, and the REGISTER_API_KEY gate will surface it.',
|
|
539
|
-
err
|
|
540
|
-
)
|
|
541
|
-
return undefined
|
|
542
|
-
}
|
|
543
|
-
throw err
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
554
|
const fetchAccountLimits = (
|
|
548
555
|
client: LighterApiClient,
|
|
549
556
|
accountIndex: number,
|
|
@@ -702,86 +709,46 @@ export const createLighterProvider = (
|
|
|
702
709
|
opts?: SDKRequestOptions
|
|
703
710
|
): Promise<AccountResponse> {
|
|
704
711
|
const client = apiClient(opts)
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
711
|
-
const assetRegistry = getAssetRegistry(requireClient(), providerKey)
|
|
712
|
-
// Shared promise: the referral read below awaits this instance's runtime
|
|
713
|
-
// metadata off the same `/providers` fetch — no second request.
|
|
714
|
-
const providersPromise = getProviders(requireClient())
|
|
715
|
-
// Shared promise: the registered-key read names the slot off the local
|
|
716
|
-
// record, and the config readout reports the record — one storage read.
|
|
717
|
-
const localKeyPromise = keyStore.get(params.address)
|
|
718
|
-
const [
|
|
719
|
-
{ providers },
|
|
720
|
-
,
|
|
721
|
-
,
|
|
722
|
-
registeredKey,
|
|
723
|
-
limitsResult,
|
|
724
|
-
localKey,
|
|
725
|
-
storedReadOnlyToken,
|
|
726
|
-
appliedReferralCode,
|
|
727
|
-
] = await Promise.all([
|
|
728
|
-
providersPromise,
|
|
729
|
-
registry.sync(),
|
|
730
|
-
assetRegistry.sync(),
|
|
731
|
-
// The local record names the only slot worth reading: no record means
|
|
732
|
-
// nothing to compare the registered key against, so the read is skipped.
|
|
733
|
-
localKeyPromise.then((key) =>
|
|
734
|
-
key === null
|
|
735
|
-
? undefined
|
|
736
|
-
: fetchRegisteredApiKey(client, account.index, key.apiKeyIndex)
|
|
737
|
-
),
|
|
738
|
-
// No token is a legitimate unauthenticated read → undefined → zero fee
|
|
739
|
-
// tier. A generic fetch error is NOT: it must propagate, never be
|
|
740
|
-
// coerced to a fabricated 0%/0% fee tier. One carve-out: a token the
|
|
741
|
-
// venue rejects even after re-resolution (stale local API key) degrades
|
|
742
|
-
// to undefined, so the REGISTER_API_KEY gate can render the recovery.
|
|
743
|
-
token === undefined
|
|
712
|
+
const accountPromise = fetchDetailedAccount(client, params.address)
|
|
713
|
+
const localKey = await keyStore.get(params.address)
|
|
714
|
+
const registeredKeyPromise =
|
|
715
|
+
localKey === null
|
|
744
716
|
? Promise.resolve(undefined)
|
|
745
|
-
:
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
readOnlyTokenManager.get(params.address, account.index),
|
|
754
|
-
// The expected code is backend-owned runtime metadata on this
|
|
755
|
-
// instance's own provider descriptor (keyed by `providerKey`, so the
|
|
756
|
-
// RH instance never compares against mainnet attribution). Metadata
|
|
757
|
-
// without a code, or no token to authenticate the read → undefined →
|
|
758
|
-
// `referralPresent: false`. The read stays SDK-direct: the token only
|
|
759
|
-
// ever goes to Lighter, never to the LI.FI backend.
|
|
760
|
-
token === undefined
|
|
761
|
-
? Promise.resolve(undefined)
|
|
762
|
-
: providersPromise.then((response) =>
|
|
763
|
-
response.providers.find((p) => p.key === providerKey)
|
|
764
|
-
?.referralCode
|
|
765
|
-
? degradeOnAuthRejection(
|
|
766
|
-
opts,
|
|
767
|
-
'/api/v1/referral/userReferrals',
|
|
768
|
-
retryOnRevoked(opts, params.address, token, (t) =>
|
|
769
|
-
fetchAppliedReferralCode(client, params.address, t)
|
|
770
|
-
)
|
|
771
|
-
)
|
|
772
|
-
: undefined
|
|
773
|
-
),
|
|
717
|
+
: fetchRegisteredApiKey(
|
|
718
|
+
client,
|
|
719
|
+
localKey.accountIndex,
|
|
720
|
+
localKey.apiKeyIndex
|
|
721
|
+
)
|
|
722
|
+
const [account, registeredKey] = await Promise.all([
|
|
723
|
+
accountPromise,
|
|
724
|
+
registeredKeyPromise,
|
|
774
725
|
])
|
|
775
726
|
|
|
776
|
-
// REGISTER_API_KEY is satisfied only when the locally-held keypair
|
|
777
|
-
// matches the key registered on-chain at this slot — existence alone is
|
|
778
|
-
// insufficient (a stale/rotated local key can't sign valid auth tokens).
|
|
779
727
|
const apiKeyRegistered =
|
|
780
728
|
registeredKey !== undefined &&
|
|
781
729
|
localKey !== null &&
|
|
782
730
|
normalizeLighterPublicKey(localKey.apiKeyPublicKey) ===
|
|
783
731
|
normalizeLighterPublicKey(registeredKey.public_key)
|
|
732
|
+
const token = apiKeyRegistered
|
|
733
|
+
? await resolveAuthToken(opts, params.address, localKey)
|
|
734
|
+
: undefined
|
|
784
735
|
|
|
736
|
+
const registry = getMarketRegistry(requireClient(), providerKey)
|
|
737
|
+
const assetRegistry = getAssetRegistry(requireClient(), providerKey)
|
|
738
|
+
const [{ providers }, , , limitsResult, storedReadOnlyToken] =
|
|
739
|
+
await Promise.all([
|
|
740
|
+
getProviders(requireClient()),
|
|
741
|
+
registry.sync(),
|
|
742
|
+
assetRegistry.sync(),
|
|
743
|
+
token === undefined
|
|
744
|
+
? Promise.resolve(undefined)
|
|
745
|
+
: retryOnRevoked(opts, params.address, token, (resolvedToken) =>
|
|
746
|
+
fetchAccountLimits(client, account.index, resolvedToken)
|
|
747
|
+
),
|
|
748
|
+
apiKeyRegistered
|
|
749
|
+
? readOnlyTokenManager.get(params.address, localKey.accountIndex)
|
|
750
|
+
: Promise.resolve(undefined),
|
|
751
|
+
])
|
|
785
752
|
const positions: Position[] = mapOpenPositions(account.positions, (id) =>
|
|
786
753
|
toPerpsMarketDisplay(registry.require(String(id)))
|
|
787
754
|
)
|
|
@@ -868,12 +835,10 @@ export const createLighterProvider = (
|
|
|
868
835
|
readOnlyTokenApproved: storedReadOnlyToken !== undefined,
|
|
869
836
|
readOnlyTokenExpiry: storedReadOnlyToken?.expiry,
|
|
870
837
|
readOnlyTokenScope: storedReadOnlyToken?.scope,
|
|
871
|
-
// True only when the authenticated `used_code` equals the backend-owned
|
|
872
|
-
// code for this instance; `appliedReferralCode` is only ever fetched
|
|
873
|
-
// when that code exists.
|
|
874
838
|
referralPresent:
|
|
875
|
-
|
|
876
|
-
|
|
839
|
+
apiKeyRegistered &&
|
|
840
|
+
instanceMeta?.referralCode !== undefined &&
|
|
841
|
+
localKey?.appliedReferralCode === instanceMeta.referralCode,
|
|
877
842
|
}
|
|
878
843
|
|
|
879
844
|
return {
|
package/src/accountConfig.ts
CHANGED
|
@@ -104,10 +104,8 @@ function projectLighterDescriptor(
|
|
|
104
104
|
satisfied: config.readOnlyTokenApproved,
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// `SET_REFERRAL`
|
|
108
|
-
//
|
|
109
|
-
// since the applied referral is a per-user, auth-gated read. Lighter referral
|
|
110
|
-
// is mutable, so an account on another integrator's code stays gateable.
|
|
107
|
+
// `SET_REFERRAL` persists its confirmed code beside the local API key.
|
|
108
|
+
// The account read compares that marker with this instance's runtime code.
|
|
111
109
|
case ActionType.SET_REFERRAL:
|
|
112
110
|
return {
|
|
113
111
|
type: descriptor.type,
|
package/src/constants.ts
CHANGED
|
@@ -228,6 +228,13 @@ const LIGHTER_CODE_ACCOUNT_NOT_FOUND = 21100
|
|
|
228
228
|
*/
|
|
229
229
|
const LIGHTER_INVALID_AUTH_CODE = 20013
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Lighter body `code` for a read-only auth token revoked at the venue.
|
|
233
|
+
*
|
|
234
|
+
* @internal
|
|
235
|
+
*/
|
|
236
|
+
const LIGHTER_TOKEN_REVOKED_CODE = 61006
|
|
237
|
+
|
|
231
238
|
/**
|
|
232
239
|
* Lighter body `code` values that mean success. Lighter is inconsistent per
|
|
233
240
|
* endpoint: `/api/v1/account` returns `code: 200`, most others return `code: 0`.
|
|
@@ -251,4 +258,5 @@ export {
|
|
|
251
258
|
LIGHTER_INVALID_AUTH_CODE,
|
|
252
259
|
LIGHTER_MUTATION_SUCCESS_CODE,
|
|
253
260
|
LIGHTER_SUCCESS_CODES,
|
|
261
|
+
LIGHTER_TOKEN_REVOKED_CODE,
|
|
254
262
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
PerpsError,
|
|
3
|
+
readValidatedRecord,
|
|
4
|
+
type StorageAdapter,
|
|
5
|
+
} from '@lifi/perps-sdk'
|
|
6
|
+
import { type LighterProviderKey, PerpsErrorCode } from '@lifi/perps-types'
|
|
3
7
|
import type { Address } from 'viem'
|
|
4
8
|
import { LIGHTER_PROVIDER_KEY } from '../constants.js'
|
|
5
9
|
|
|
@@ -30,6 +34,8 @@ export interface LighterApiKey {
|
|
|
30
34
|
apiKeyPrivateKey: string
|
|
31
35
|
/** Corresponding public key, registered via ChangePubKey. */
|
|
32
36
|
apiKeyPublicKey: string
|
|
37
|
+
/** Referral code confirmed by the standard-token SET_REFERRAL action. */
|
|
38
|
+
appliedReferralCode?: string
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const isLighterApiKeyOf =
|
|
@@ -44,6 +50,7 @@ const isLighterApiKeyOf =
|
|
|
44
50
|
apiKeyIndex,
|
|
45
51
|
apiKeyPrivateKey,
|
|
46
52
|
apiKeyPublicKey,
|
|
53
|
+
appliedReferralCode,
|
|
47
54
|
} = value as Record<string, unknown>
|
|
48
55
|
return (
|
|
49
56
|
providerKey === owner &&
|
|
@@ -54,7 +61,10 @@ const isLighterApiKeyOf =
|
|
|
54
61
|
typeof apiKeyPrivateKey === 'string' &&
|
|
55
62
|
apiKeyPrivateKey.length > 0 &&
|
|
56
63
|
typeof apiKeyPublicKey === 'string' &&
|
|
57
|
-
apiKeyPublicKey.length > 0
|
|
64
|
+
apiKeyPublicKey.length > 0 &&
|
|
65
|
+
(appliedReferralCode === undefined ||
|
|
66
|
+
(typeof appliedReferralCode === 'string' &&
|
|
67
|
+
appliedReferralCode.length > 0))
|
|
58
68
|
)
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -140,6 +150,21 @@ export class LighterKeyStore {
|
|
|
140
150
|
await this.storage.set(key, JSON.stringify(record))
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
async markReferralApplied(
|
|
154
|
+
address: Address,
|
|
155
|
+
appliedReferralCode: string
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const current = await this.get(address)
|
|
158
|
+
if (current === null) {
|
|
159
|
+
throw new PerpsError(
|
|
160
|
+
PerpsErrorCode.SDKError,
|
|
161
|
+
`Cannot persist the Lighter referral marker without an API key for ${address}`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
const { providerKey: _providerKey, ...value } = current
|
|
165
|
+
await this.set(address, { ...value, appliedReferralCode })
|
|
166
|
+
}
|
|
167
|
+
|
|
143
168
|
/**
|
|
144
169
|
* Remove the persisted API key for an L1 address and clear its in-memory
|
|
145
170
|
* cache entry.
|
|
@@ -75,6 +75,7 @@ export type LighterTokenFetcher = (params: {
|
|
|
75
75
|
expiry: number
|
|
76
76
|
subAccountAccess: boolean
|
|
77
77
|
scopes: string
|
|
78
|
+
fetchImpl?: typeof fetch
|
|
78
79
|
}) => Promise<LighterCreateTokenResponse>
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -127,6 +128,7 @@ export type LighterTokenListFetcher = (params: {
|
|
|
127
128
|
url: string
|
|
128
129
|
authorization: string
|
|
129
130
|
accountIndex: number
|
|
131
|
+
fetchImpl?: typeof fetch
|
|
130
132
|
}) => Promise<LighterListTokensResponse>
|
|
131
133
|
|
|
132
134
|
/**
|
|
@@ -139,6 +141,7 @@ export type LighterTokenRevokeFetcher = (params: {
|
|
|
139
141
|
authorization: string
|
|
140
142
|
tokenId: number
|
|
141
143
|
accountIndex: number
|
|
144
|
+
fetchImpl?: typeof fetch
|
|
142
145
|
}) => Promise<LighterRevokeTokenResponse>
|
|
143
146
|
|
|
144
147
|
/**
|
|
@@ -165,6 +168,8 @@ export interface LighterReadOnlyTokenManagerOptions {
|
|
|
165
168
|
listFetcher?: LighterTokenListFetcher
|
|
166
169
|
/** Override the revoke POST. Defaults to a `fetch`-based implementation. */
|
|
167
170
|
revokeFetcher?: LighterTokenRevokeFetcher
|
|
171
|
+
/** Transport used by the default endpoint fetchers. Defaults to `fetch`. */
|
|
172
|
+
fetchImpl?: typeof fetch
|
|
168
173
|
/** Clock injection for testing expiry logic. Defaults to `Date.now`. */
|
|
169
174
|
now?: () => number
|
|
170
175
|
}
|
|
@@ -259,6 +264,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
259
264
|
private readonly fetcher: LighterTokenFetcher
|
|
260
265
|
private readonly listFetcher: LighterTokenListFetcher
|
|
261
266
|
private readonly revokeFetcher: LighterTokenRevokeFetcher
|
|
267
|
+
private readonly fetchImpl: typeof fetch | undefined
|
|
262
268
|
private readonly now: () => number
|
|
263
269
|
private readonly cache = new Map<string, LighterReadOnlyToken>()
|
|
264
270
|
|
|
@@ -270,6 +276,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
270
276
|
this.listFetcher = options.listFetcher ?? defaultLighterTokenListFetcher
|
|
271
277
|
this.revokeFetcher =
|
|
272
278
|
options.revokeFetcher ?? defaultLighterTokenRevokeFetcher
|
|
279
|
+
this.fetchImpl = options.fetchImpl
|
|
273
280
|
this.now = options.now ?? (() => Date.now())
|
|
274
281
|
}
|
|
275
282
|
|
|
@@ -398,10 +405,31 @@ export class LighterReadOnlyTokenManager {
|
|
|
398
405
|
authorization: string,
|
|
399
406
|
inputs: ApproveReadOnlyTokenInputs
|
|
400
407
|
): Promise<ApproveReadOnlyTokenResult> {
|
|
401
|
-
|
|
408
|
+
await this.revokeStaleOwnTokens(
|
|
409
|
+
authorization,
|
|
410
|
+
inputs.address,
|
|
411
|
+
inputs.accountIndex
|
|
412
|
+
)
|
|
413
|
+
return this.createAndPersist(authorization, inputs)
|
|
414
|
+
}
|
|
402
415
|
|
|
403
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Replace a token that Lighter explicitly reports as revoked.
|
|
418
|
+
* The venue already revoked the row, so this path skips registry cleanup.
|
|
419
|
+
*/
|
|
420
|
+
async replaceRevoked(
|
|
421
|
+
authorization: string,
|
|
422
|
+
inputs: ApproveReadOnlyTokenInputs
|
|
423
|
+
): Promise<ApproveReadOnlyTokenResult> {
|
|
424
|
+
await this.remove(inputs.address, inputs.accountIndex)
|
|
425
|
+
return this.createAndPersist(authorization, inputs)
|
|
426
|
+
}
|
|
404
427
|
|
|
428
|
+
private async createAndPersist(
|
|
429
|
+
authorization: string,
|
|
430
|
+
inputs: ApproveReadOnlyTokenInputs
|
|
431
|
+
): Promise<ApproveReadOnlyTokenResult> {
|
|
432
|
+
const { address, accountIndex, expirySeconds, scope } = inputs
|
|
405
433
|
const response = await this.fetcher({
|
|
406
434
|
url: `${this.lighterApiUrl}/api/v1/tokens/create`,
|
|
407
435
|
authorization,
|
|
@@ -410,6 +438,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
410
438
|
expiry: expirySeconds,
|
|
411
439
|
subAccountAccess: scope === 'all',
|
|
412
440
|
scopes: 'read.*',
|
|
441
|
+
fetchImpl: this.fetchImpl,
|
|
413
442
|
})
|
|
414
443
|
|
|
415
444
|
const token: LighterReadOnlyToken = {
|
|
@@ -457,6 +486,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
457
486
|
url: `${this.lighterApiUrl}/api/v1/tokens`,
|
|
458
487
|
authorization,
|
|
459
488
|
accountIndex,
|
|
489
|
+
fetchImpl: this.fetchImpl,
|
|
460
490
|
})
|
|
461
491
|
const nowSeconds = Math.floor(this.now() / 1000)
|
|
462
492
|
const stale = rows
|
|
@@ -476,6 +506,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
476
506
|
authorization,
|
|
477
507
|
tokenId: row.token_id,
|
|
478
508
|
accountIndex,
|
|
509
|
+
fetchImpl: this.fetchImpl,
|
|
479
510
|
})
|
|
480
511
|
} catch (err) {
|
|
481
512
|
console.warn(
|
|
@@ -508,6 +539,7 @@ export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
|
|
|
508
539
|
expiry,
|
|
509
540
|
subAccountAccess,
|
|
510
541
|
scopes,
|
|
542
|
+
fetchImpl,
|
|
511
543
|
}) => {
|
|
512
544
|
const form = new FormData()
|
|
513
545
|
form.set('name', name)
|
|
@@ -516,7 +548,7 @@ export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
|
|
|
516
548
|
form.set('sub_account_access', String(subAccountAccess))
|
|
517
549
|
form.set('scopes', scopes)
|
|
518
550
|
|
|
519
|
-
const response = await fetch(url, {
|
|
551
|
+
const response = await (fetchImpl ?? fetch)(url, {
|
|
520
552
|
method: 'POST',
|
|
521
553
|
headers: { authorization },
|
|
522
554
|
body: form,
|
|
@@ -544,10 +576,11 @@ export const defaultLighterTokenListFetcher: LighterTokenListFetcher = async ({
|
|
|
544
576
|
url,
|
|
545
577
|
authorization,
|
|
546
578
|
accountIndex,
|
|
579
|
+
fetchImpl,
|
|
547
580
|
}) => {
|
|
548
581
|
const query = new URLSearchParams({ account_index: String(accountIndex) })
|
|
549
582
|
|
|
550
|
-
const response = await fetch(`${url}?${query.toString()}`, {
|
|
583
|
+
const response = await (fetchImpl ?? fetch)(`${url}?${query.toString()}`, {
|
|
551
584
|
headers: { authorization },
|
|
552
585
|
})
|
|
553
586
|
|
|
@@ -570,13 +603,13 @@ export const defaultLighterTokenListFetcher: LighterTokenListFetcher = async ({
|
|
|
570
603
|
* @internal
|
|
571
604
|
*/
|
|
572
605
|
export const defaultLighterTokenRevokeFetcher: LighterTokenRevokeFetcher =
|
|
573
|
-
async ({ url, authorization, tokenId, accountIndex }) => {
|
|
606
|
+
async ({ url, authorization, tokenId, accountIndex, fetchImpl }) => {
|
|
574
607
|
const form = new URLSearchParams({
|
|
575
608
|
token_id: String(tokenId),
|
|
576
609
|
account_index: String(accountIndex),
|
|
577
610
|
})
|
|
578
611
|
|
|
579
|
-
const response = await fetch(url, {
|
|
612
|
+
const response = await (fetchImpl ?? fetch)(url, {
|
|
580
613
|
method: 'POST',
|
|
581
614
|
headers: {
|
|
582
615
|
authorization,
|
|
@@ -304,11 +304,14 @@ async function signRegisterApiKey(
|
|
|
304
304
|
l1Signature
|
|
305
305
|
)
|
|
306
306
|
|
|
307
|
+
// The referral marker belongs to the L1 address, not to the key slot.
|
|
308
|
+
const previousKey = await deps.keyStore.get(address)
|
|
307
309
|
await deps.keyStore.set(address, {
|
|
308
310
|
accountIndex,
|
|
309
311
|
apiKeyIndex,
|
|
310
312
|
apiKeyPrivateKey: keypair.privateKey,
|
|
311
313
|
apiKeyPublicKey: keypair.publicKey,
|
|
314
|
+
appliedReferralCode: previousKey?.appliedReferralCode,
|
|
312
315
|
})
|
|
313
316
|
|
|
314
317
|
return {
|
|
@@ -461,12 +464,15 @@ async function executeTokenAuthMutation(
|
|
|
461
464
|
accountIndex: apiKey.accountIndex,
|
|
462
465
|
})
|
|
463
466
|
|
|
467
|
+
let appliedReferralCode: string | undefined
|
|
464
468
|
if (step.wasmSignParams.kind === 'referralUse') {
|
|
465
469
|
const { referral_code } = step.wasmSignParams as { referral_code?: string }
|
|
470
|
+
appliedReferralCode = referral_code
|
|
466
471
|
if (
|
|
467
472
|
typeof referral_code === 'string' &&
|
|
468
473
|
(await isReferralAlreadyApplied(deps, address, authToken, referral_code))
|
|
469
474
|
) {
|
|
475
|
+
await deps.keyStore.markReferralApplied(address, referral_code)
|
|
470
476
|
return
|
|
471
477
|
}
|
|
472
478
|
}
|
|
@@ -485,6 +491,10 @@ async function executeTokenAuthMutation(
|
|
|
485
491
|
`Lighter ${step.action} rejected (code ${code ?? status})${suffix}`
|
|
486
492
|
)
|
|
487
493
|
}
|
|
494
|
+
|
|
495
|
+
if (appliedReferralCode !== undefined) {
|
|
496
|
+
await deps.keyStore.markReferralApplied(address, appliedReferralCode)
|
|
497
|
+
}
|
|
488
498
|
}
|
|
489
499
|
|
|
490
500
|
/**
|