@lifi/perps-sdk-provider-lighter 17.0.1 → 17.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.map +1 -1
- package/dist/cjs/LighterProvider.js +1 -0
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/accountConfig.d.ts.map +1 -1
- package/dist/cjs/accountConfig.js +12 -4
- package/dist/cjs/accountConfig.js.map +1 -1
- package/dist/cjs/constants.d.ts +1 -0
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +2 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts +2 -1
- package/dist/cjs/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/cjs/signers/LighterKeyStore.js +8 -6
- package/dist/cjs/signers/LighterKeyStore.js.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts +42 -0
- package/dist/cjs/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/cjs/signers/LighterReadOnlyTokenManager.js +87 -6
- 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 +9 -5
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/types/fill.d.ts +2 -0
- package/dist/cjs/types/fill.d.ts.map +1 -1
- package/dist/cjs/utils/mapFill.d.ts.map +1 -1
- package/dist/cjs/utils/mapFill.js +8 -0
- package/dist/cjs/utils/mapFill.js.map +1 -1
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +1 -0
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/accountConfig.d.ts.map +1 -1
- package/dist/esm/accountConfig.js +29 -16
- package/dist/esm/accountConfig.js.map +1 -1
- package/dist/esm/constants.d.ts +9 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +9 -0
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/signers/LighterKeyStore.d.ts +12 -4
- package/dist/esm/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/esm/signers/LighterKeyStore.js +16 -15
- package/dist/esm/signers/LighterKeyStore.js.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts +112 -12
- package/dist/esm/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/esm/signers/LighterReadOnlyTokenManager.js +119 -8
- 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 +15 -9
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/types/fill.d.ts +2 -0
- package/dist/esm/types/fill.d.ts.map +1 -1
- package/dist/esm/utils/mapFill.d.ts.map +1 -1
- package/dist/esm/utils/mapFill.js +16 -1
- package/dist/esm/utils/mapFill.js.map +1 -1
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/accountConfig.d.ts.map +1 -1
- package/dist/types/constants.d.ts +9 -0
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/signers/LighterKeyStore.d.ts +12 -4
- package/dist/types/signers/LighterKeyStore.d.ts.map +1 -1
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts +112 -12
- package/dist/types/signers/LighterReadOnlyTokenManager.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/types/fill.d.ts +2 -0
- package/dist/types/types/fill.d.ts.map +1 -1
- package/dist/types/utils/mapFill.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/LighterProvider.ts +1 -0
- package/src/accountConfig.ts +33 -16
- package/src/constants.ts +10 -0
- package/src/signers/LighterKeyStore.ts +50 -29
- package/src/signers/LighterReadOnlyTokenManager.ts +245 -18
- package/src/signers/signActions.ts +18 -11
- package/src/types/fill.ts +6 -0
- package/src/utils/mapFill.ts +21 -1
package/src/constants.ts
CHANGED
|
@@ -183,6 +183,16 @@ export const DEFAULT_TRADES_LIMIT = 50
|
|
|
183
183
|
*/
|
|
184
184
|
export const LIGHTER_FEE_TICK_SCALE = 1_000_000
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Scale of the integer `initial_margin_fraction` values Lighter publishes on
|
|
188
|
+
* trade rows and order-book details: the integer is a percent times 100, so
|
|
189
|
+
* `500` is 5.00% and `200` is 2.00%. Account positions publish the same
|
|
190
|
+
* fraction as a plain percent string ("5.00") instead.
|
|
191
|
+
*
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
export const LIGHTER_IMF_PERCENT_SCALE = 100
|
|
195
|
+
|
|
186
196
|
/**
|
|
187
197
|
* Lighter's public base fee tier (Standard Account): 0% maker / 0% taker on all
|
|
188
198
|
* perps markets. Per-account Premium tiers exist but require auth to resolve;
|
|
@@ -4,10 +4,7 @@ import type { Address } from 'viem'
|
|
|
4
4
|
import { LIGHTER_PROVIDER_KEY } from '../constants.js'
|
|
5
5
|
|
|
6
6
|
// The private key here is a Lighter custom keypair — generated via WASM
|
|
7
|
-
// GenerateAPIKey — not an Ethereum private key.
|
|
8
|
-
// by the resolved provider instance key so two Lighter instances sharing a
|
|
9
|
-
// storage adapter (e.g. `lighter` and `lighter-rh`) never clobber each other's
|
|
10
|
-
// API key.
|
|
7
|
+
// GenerateAPIKey — not an Ethereum private key.
|
|
11
8
|
|
|
12
9
|
const STORAGE_PREFIX = 'lifi-perps-lighter-key'
|
|
13
10
|
|
|
@@ -19,6 +16,12 @@ const STORAGE_PREFIX = 'lifi-perps-lighter-key'
|
|
|
19
16
|
* @public
|
|
20
17
|
*/
|
|
21
18
|
export interface LighterApiKey {
|
|
19
|
+
/**
|
|
20
|
+
* Lighter instance the record belongs to. A record read by any other
|
|
21
|
+
* instance is discarded, so its key material never signs on a chain that
|
|
22
|
+
* does not hold it.
|
|
23
|
+
*/
|
|
24
|
+
providerKey: LighterProviderKey
|
|
22
25
|
/** Lighter account index, looked up once via accountsByL1Address. */
|
|
23
26
|
accountIndex: number
|
|
24
27
|
/** API key slot (0-255), as named by the backend registration payload. */
|
|
@@ -29,28 +32,37 @@ export interface LighterApiKey {
|
|
|
29
32
|
apiKeyPublicKey: string
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const isLighterApiKeyOf =
|
|
36
|
+
(owner: LighterProviderKey) =>
|
|
37
|
+
(value: unknown): value is LighterApiKey => {
|
|
38
|
+
if (typeof value !== 'object' || value === null) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
const {
|
|
42
|
+
providerKey,
|
|
43
|
+
accountIndex,
|
|
44
|
+
apiKeyIndex,
|
|
45
|
+
apiKeyPrivateKey,
|
|
46
|
+
apiKeyPublicKey,
|
|
47
|
+
} = value as Record<string, unknown>
|
|
48
|
+
return (
|
|
49
|
+
providerKey === owner &&
|
|
50
|
+
typeof accountIndex === 'number' &&
|
|
51
|
+
Number.isFinite(accountIndex) &&
|
|
52
|
+
typeof apiKeyIndex === 'number' &&
|
|
53
|
+
Number.isFinite(apiKeyIndex) &&
|
|
54
|
+
typeof apiKeyPrivateKey === 'string' &&
|
|
55
|
+
apiKeyPrivateKey.length > 0 &&
|
|
56
|
+
typeof apiKeyPublicKey === 'string' &&
|
|
57
|
+
apiKeyPublicKey.length > 0
|
|
58
|
+
)
|
|
35
59
|
}
|
|
36
|
-
const { accountIndex, apiKeyIndex, apiKeyPrivateKey, apiKeyPublicKey } =
|
|
37
|
-
value as Record<string, unknown>
|
|
38
|
-
return (
|
|
39
|
-
typeof accountIndex === 'number' &&
|
|
40
|
-
Number.isFinite(accountIndex) &&
|
|
41
|
-
typeof apiKeyIndex === 'number' &&
|
|
42
|
-
Number.isFinite(apiKeyIndex) &&
|
|
43
|
-
typeof apiKeyPrivateKey === 'string' &&
|
|
44
|
-
apiKeyPrivateKey.length > 0 &&
|
|
45
|
-
typeof apiKeyPublicKey === 'string' &&
|
|
46
|
-
apiKeyPublicKey.length > 0
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
60
|
|
|
50
61
|
/**
|
|
51
62
|
* Storage-backed cache for Lighter API keys. Records are namespaced by L1
|
|
52
|
-
* address and provider instance
|
|
53
|
-
* {@link StorageAdapter}
|
|
63
|
+
* address and provider instance, and each record names its own instance, so
|
|
64
|
+
* multiple Lighter deployments can share one {@link StorageAdapter} and no
|
|
65
|
+
* instance ever reads another instance's key material.
|
|
54
66
|
*
|
|
55
67
|
* @public
|
|
56
68
|
*/
|
|
@@ -82,8 +94,8 @@ export class LighterKeyStore {
|
|
|
82
94
|
this.providerKey = providerKey
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
// The default instance keeps the legacy, un-namespaced
|
|
86
|
-
//
|
|
97
|
+
// The default instance keeps the legacy, un-namespaced slot name; only
|
|
98
|
+
// additional instances get a segment.
|
|
87
99
|
private storageKey(address: Address): string {
|
|
88
100
|
const lower = address.toLowerCase()
|
|
89
101
|
return this.providerKey === LIGHTER_PROVIDER_KEY
|
|
@@ -101,7 +113,11 @@ export class LighterKeyStore {
|
|
|
101
113
|
if (cached) {
|
|
102
114
|
return cached
|
|
103
115
|
}
|
|
104
|
-
const parsed = await readValidatedRecord(
|
|
116
|
+
const parsed = await readValidatedRecord(
|
|
117
|
+
this.storage,
|
|
118
|
+
key,
|
|
119
|
+
isLighterApiKeyOf(this.providerKey)
|
|
120
|
+
)
|
|
105
121
|
if (!parsed) {
|
|
106
122
|
return null
|
|
107
123
|
}
|
|
@@ -111,12 +127,17 @@ export class LighterKeyStore {
|
|
|
111
127
|
|
|
112
128
|
/**
|
|
113
129
|
* Persist an API key for an L1 address, replacing any existing record under
|
|
114
|
-
* that provider namespace.
|
|
130
|
+
* that provider namespace. The store stamps the record with its own instance
|
|
131
|
+
* key, so callers never name the instance themselves.
|
|
115
132
|
*/
|
|
116
|
-
async set(
|
|
133
|
+
async set(
|
|
134
|
+
address: Address,
|
|
135
|
+
value: Omit<LighterApiKey, 'providerKey'>
|
|
136
|
+
): Promise<void> {
|
|
117
137
|
const key = this.storageKey(address)
|
|
118
|
-
this.
|
|
119
|
-
|
|
138
|
+
const record: LighterApiKey = { ...value, providerKey: this.providerKey }
|
|
139
|
+
this.cache.set(key, record)
|
|
140
|
+
await this.storage.set(key, JSON.stringify(record))
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
/**
|
|
@@ -17,7 +17,7 @@ import { DEFAULT_LIGHTER_REST_URL, LIGHTER_PROVIDER_KEY } from '../constants.js'
|
|
|
17
17
|
* Default token name persisted alongside Lighter's `tokens/create` row.
|
|
18
18
|
* Lighter requires a non-empty `name` form field; the literal here is what
|
|
19
19
|
* surfaces in Lighter's UI listing under `app.lighter.xyz/read-only-tokens`.
|
|
20
|
-
* @
|
|
20
|
+
* @internal
|
|
21
21
|
*/
|
|
22
22
|
export const DEFAULT_READ_ONLY_TOKEN_NAME = 'LI.FI Perps'
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ export const DEFAULT_READ_ONLY_TOKEN_NAME = 'LI.FI Perps'
|
|
|
26
26
|
* Lighter's opaque `ro:{accountIndex}:{scope}:{expiry}:{rand}` bearer; the
|
|
27
27
|
* SDK never parses it — `expiry`/`scope`/`accountIndex` are create-time inputs
|
|
28
28
|
* we keep alongside so consumers can render expiry UX without re-fetching.
|
|
29
|
-
* @
|
|
29
|
+
* @internal
|
|
30
30
|
*/
|
|
31
31
|
export interface LighterReadOnlyToken {
|
|
32
32
|
/** Opaque bearer string — never parse client-side. */
|
|
@@ -37,19 +37,27 @@ export interface LighterReadOnlyToken {
|
|
|
37
37
|
scope: 'single' | 'all'
|
|
38
38
|
/** Lighter L2 account index the token authorises. */
|
|
39
39
|
accountIndex: number
|
|
40
|
+
/**
|
|
41
|
+
* Lighter's monotonic registry row id. Optional: a record persisted without
|
|
42
|
+
* it stays valid, and the cleanup pass then identifies its row by the
|
|
43
|
+
* bearer string instead.
|
|
44
|
+
*/
|
|
45
|
+
tokenId?: number
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
|
-
* Lighter `POST /api/v1/tokens/create` response shape
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @
|
|
49
|
+
* Lighter `POST /api/v1/tokens/create` response shape — the subset of
|
|
50
|
+
* Lighter's `RespPostApiToken` the SDK consumes. Lighter documents further
|
|
51
|
+
* fields (`name`, `revoked`, `sub_account_access`) the SDK does not need.
|
|
52
|
+
* @internal
|
|
47
53
|
*/
|
|
48
54
|
export interface LighterCreateTokenResponse {
|
|
49
55
|
api_token: string
|
|
50
56
|
account_index: number
|
|
51
57
|
expiry: number
|
|
52
58
|
scopes: string
|
|
59
|
+
/** Monotonic registry row id Lighter assigns to the new row. */
|
|
60
|
+
token_id: number
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -57,7 +65,7 @@ export interface LighterCreateTokenResponse {
|
|
|
57
65
|
* endpoint. Returning a parsed {@link LighterCreateTokenResponse} keeps the
|
|
58
66
|
* create code free of fetch/multipart plumbing and lets tests drop a fixture
|
|
59
67
|
* in without spinning a mock server.
|
|
60
|
-
* @
|
|
68
|
+
* @internal
|
|
61
69
|
*/
|
|
62
70
|
export type LighterTokenFetcher = (params: {
|
|
63
71
|
url: string
|
|
@@ -69,12 +77,76 @@ export type LighterTokenFetcher = (params: {
|
|
|
69
77
|
scopes: string
|
|
70
78
|
}) => Promise<LighterCreateTokenResponse>
|
|
71
79
|
|
|
80
|
+
/**
|
|
81
|
+
* One row of Lighter's read-only token registry, matching Lighter's
|
|
82
|
+
* `ApiToken`. `name` carries the label supplied at create time, which is how
|
|
83
|
+
* the SDK recognises a row it owns.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export interface LighterApiToken {
|
|
87
|
+
token_id: number
|
|
88
|
+
api_token: string
|
|
89
|
+
name: string
|
|
90
|
+
account_index: number
|
|
91
|
+
expiry: number
|
|
92
|
+
sub_account_access: boolean
|
|
93
|
+
revoked: boolean
|
|
94
|
+
scopes: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lighter `GET /api/v1/tokens` response shape, matching `RespGetApiTokens`.
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export interface LighterListTokensResponse {
|
|
102
|
+
code: number
|
|
103
|
+
message?: string
|
|
104
|
+
api_tokens: LighterApiToken[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lighter `POST /api/v1/tokens/revoke` response shape, matching Lighter's
|
|
109
|
+
* `RespRevokeApiToken`.
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
export interface LighterRevokeTokenResponse {
|
|
113
|
+
code: number
|
|
114
|
+
message?: string
|
|
115
|
+
token_id: number
|
|
116
|
+
revoked: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Function injected for the HTTP boundary against Lighter's
|
|
121
|
+
* `GET /api/v1/tokens` endpoint. Injected for the same reason as
|
|
122
|
+
* {@link LighterTokenFetcher}: a unit spec supplies a registry fixture
|
|
123
|
+
* without a mock server.
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export type LighterTokenListFetcher = (params: {
|
|
127
|
+
url: string
|
|
128
|
+
authorization: string
|
|
129
|
+
accountIndex: number
|
|
130
|
+
}) => Promise<LighterListTokensResponse>
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Function injected for the HTTP boundary against Lighter's
|
|
134
|
+
* `POST /api/v1/tokens/revoke` endpoint.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export type LighterTokenRevokeFetcher = (params: {
|
|
138
|
+
url: string
|
|
139
|
+
authorization: string
|
|
140
|
+
tokenId: number
|
|
141
|
+
accountIndex: number
|
|
142
|
+
}) => Promise<LighterRevokeTokenResponse>
|
|
143
|
+
|
|
72
144
|
/**
|
|
73
145
|
* Dependencies and overrides for {@link LighterReadOnlyTokenManager}.
|
|
74
146
|
* Storage and token-fetching defaults target the browser's local storage and
|
|
75
147
|
* Lighter's mainnet REST API.
|
|
76
148
|
*
|
|
77
|
-
* @
|
|
149
|
+
* @internal
|
|
78
150
|
*/
|
|
79
151
|
export interface LighterReadOnlyTokenManagerOptions {
|
|
80
152
|
storage?: StorageAdapter
|
|
@@ -89,6 +161,10 @@ export interface LighterReadOnlyTokenManagerOptions {
|
|
|
89
161
|
lighterApiUrl?: string
|
|
90
162
|
/** Override the multipart POST. Defaults to a `fetch`-based implementation. */
|
|
91
163
|
fetcher?: LighterTokenFetcher
|
|
164
|
+
/** Override the registry list GET. Defaults to a `fetch`-based implementation. */
|
|
165
|
+
listFetcher?: LighterTokenListFetcher
|
|
166
|
+
/** Override the revoke POST. Defaults to a `fetch`-based implementation. */
|
|
167
|
+
revokeFetcher?: LighterTokenRevokeFetcher
|
|
92
168
|
/** Clock injection for testing expiry logic. Defaults to `Date.now`. */
|
|
93
169
|
now?: () => number
|
|
94
170
|
}
|
|
@@ -97,7 +173,7 @@ export interface LighterReadOnlyTokenManagerOptions {
|
|
|
97
173
|
* Input to the read-only-token approval flow. Extends the shared approval
|
|
98
174
|
* parameters with the L1 address whose wallet authorizes token creation.
|
|
99
175
|
*
|
|
100
|
-
* @
|
|
176
|
+
* @internal
|
|
101
177
|
*/
|
|
102
178
|
export interface ApproveReadOnlyTokenInputs extends ApproveReadOnlyTokenParams {
|
|
103
179
|
/** L1 wallet address that signs the create message. */
|
|
@@ -108,7 +184,7 @@ export interface ApproveReadOnlyTokenInputs extends ApproveReadOnlyTokenParams {
|
|
|
108
184
|
* Result of approving or creating a Lighter read-only token. `config` is the
|
|
109
185
|
* account-state projection callers can persist alongside the token.
|
|
110
186
|
*
|
|
111
|
-
* @
|
|
187
|
+
* @internal
|
|
112
188
|
*/
|
|
113
189
|
export interface ApproveReadOnlyTokenResult {
|
|
114
190
|
token: LighterReadOnlyToken
|
|
@@ -131,10 +207,12 @@ const isLighterReadOnlyToken = (
|
|
|
131
207
|
if (typeof value !== 'object' || value === null) {
|
|
132
208
|
return false
|
|
133
209
|
}
|
|
134
|
-
const { token, expiry, scope, accountIndex } = value as Record<
|
|
210
|
+
const { token, expiry, scope, accountIndex, tokenId } = value as Record<
|
|
135
211
|
string,
|
|
136
212
|
unknown
|
|
137
213
|
>
|
|
214
|
+
// Accept a record that carries no `tokenId`: rejecting one would discard a
|
|
215
|
+
// usable token and force an unnecessary replacement.
|
|
138
216
|
return (
|
|
139
217
|
typeof token === 'string' &&
|
|
140
218
|
token.length > 0 &&
|
|
@@ -142,10 +220,19 @@ const isLighterReadOnlyToken = (
|
|
|
142
220
|
Number.isFinite(expiry) &&
|
|
143
221
|
(scope === 'single' || scope === 'all') &&
|
|
144
222
|
typeof accountIndex === 'number' &&
|
|
145
|
-
Number.isFinite(accountIndex)
|
|
223
|
+
Number.isFinite(accountIndex) &&
|
|
224
|
+
(tokenId === undefined ||
|
|
225
|
+
(typeof tokenId === 'number' && Number.isFinite(tokenId)))
|
|
146
226
|
)
|
|
147
227
|
}
|
|
148
228
|
|
|
229
|
+
const isLiveRow = (
|
|
230
|
+
row: LighterApiToken,
|
|
231
|
+
live: LighterReadOnlyToken | undefined
|
|
232
|
+
): boolean =>
|
|
233
|
+
live !== undefined &&
|
|
234
|
+
(row.token_id === live.tokenId || row.api_token === live.token)
|
|
235
|
+
|
|
149
236
|
/**
|
|
150
237
|
* Manage the per-account Lighter read-only token alongside the existing
|
|
151
238
|
* `(L1 address, account index)`-scoped storage adapter pattern. Takes an
|
|
@@ -163,13 +250,15 @@ const isLighterReadOnlyToken = (
|
|
|
163
250
|
* a fully compromised page — a same-origin script can still drive this manager
|
|
164
251
|
* to decrypt. Blast radius is limited to reads: the token cannot sign orders
|
|
165
252
|
* or move funds.
|
|
166
|
-
* @
|
|
253
|
+
* @internal
|
|
167
254
|
*/
|
|
168
255
|
export class LighterReadOnlyTokenManager {
|
|
169
256
|
private readonly storage: StorageAdapter
|
|
170
257
|
private readonly providerKey: LighterProviderKey
|
|
171
258
|
private readonly lighterApiUrl: string
|
|
172
259
|
private readonly fetcher: LighterTokenFetcher
|
|
260
|
+
private readonly listFetcher: LighterTokenListFetcher
|
|
261
|
+
private readonly revokeFetcher: LighterTokenRevokeFetcher
|
|
173
262
|
private readonly now: () => number
|
|
174
263
|
private readonly cache = new Map<string, LighterReadOnlyToken>()
|
|
175
264
|
|
|
@@ -178,6 +267,9 @@ export class LighterReadOnlyTokenManager {
|
|
|
178
267
|
this.providerKey = options.providerKey ?? LIGHTER_PROVIDER_KEY
|
|
179
268
|
this.lighterApiUrl = options.lighterApiUrl ?? DEFAULT_LIGHTER_REST_URL
|
|
180
269
|
this.fetcher = options.fetcher ?? defaultLighterTokenFetcher
|
|
270
|
+
this.listFetcher = options.listFetcher ?? defaultLighterTokenListFetcher
|
|
271
|
+
this.revokeFetcher =
|
|
272
|
+
options.revokeFetcher ?? defaultLighterTokenRevokeFetcher
|
|
181
273
|
this.now = options.now ?? (() => Date.now())
|
|
182
274
|
}
|
|
183
275
|
|
|
@@ -189,6 +281,19 @@ export class LighterReadOnlyTokenManager {
|
|
|
189
281
|
return token.expiry * 1000 <= this.now()
|
|
190
282
|
}
|
|
191
283
|
|
|
284
|
+
/** Stored record for the pair, whether or not it is past its `expiry`. */
|
|
285
|
+
private async readStoredToken(
|
|
286
|
+
address: Address,
|
|
287
|
+
accountIndex: number
|
|
288
|
+
): Promise<LighterReadOnlyToken | undefined> {
|
|
289
|
+
const key = this.storageKey(address, accountIndex)
|
|
290
|
+
return (
|
|
291
|
+
this.cache.get(key) ??
|
|
292
|
+
(await readValidatedRecord(this.storage, key, isLighterReadOnlyToken)) ??
|
|
293
|
+
undefined
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
192
297
|
/**
|
|
193
298
|
* Return the stored token for the `(address, accountIndex)` pair, or
|
|
194
299
|
* `undefined` if absent or past its `expiry`. Never surfaces an expired
|
|
@@ -226,10 +331,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
226
331
|
accountIndex: number,
|
|
227
332
|
thresholdDays = 30
|
|
228
333
|
): Promise<boolean> {
|
|
229
|
-
const
|
|
230
|
-
const token =
|
|
231
|
-
this.cache.get(key) ??
|
|
232
|
-
(await readValidatedRecord(this.storage, key, isLighterReadOnlyToken))
|
|
334
|
+
const token = await this.readStoredToken(address, accountIndex)
|
|
233
335
|
if (!token) {
|
|
234
336
|
return false
|
|
235
337
|
}
|
|
@@ -272,6 +374,9 @@ export class LighterReadOnlyTokenManager {
|
|
|
272
374
|
* and persists the returned `ro:` bearer alongside its
|
|
273
375
|
* `expiry`/`scope`/`accountIndex`.
|
|
274
376
|
*
|
|
377
|
+
* Before the create call, the flow revokes this SDK's own stale registry
|
|
378
|
+
* rows. That pass is best-effort: a failure is logged and the create runs.
|
|
379
|
+
*
|
|
275
380
|
* `authorization` MUST be a **standard** Lighter auth token — one created by
|
|
276
381
|
* the account's API key (`createAuthToken` / the WASM signer), NOT an L1
|
|
277
382
|
* wallet signature. Lighter authenticates the create request with that token
|
|
@@ -295,6 +400,8 @@ export class LighterReadOnlyTokenManager {
|
|
|
295
400
|
): Promise<ApproveReadOnlyTokenResult> {
|
|
296
401
|
const { address, accountIndex, expirySeconds, scope } = inputs
|
|
297
402
|
|
|
403
|
+
await this.revokeStaleOwnTokens(authorization, address, accountIndex)
|
|
404
|
+
|
|
298
405
|
const response = await this.fetcher({
|
|
299
406
|
url: `${this.lighterApiUrl}/api/v1/tokens/create`,
|
|
300
407
|
authorization,
|
|
@@ -313,6 +420,12 @@ export class LighterReadOnlyTokenManager {
|
|
|
313
420
|
// `get()` looks up by, else a divergent echo orphans the token and re-creates every read.
|
|
314
421
|
scope,
|
|
315
422
|
accountIndex,
|
|
423
|
+
// Lighter declares `token_id` required but the response is unvalidated.
|
|
424
|
+
// A non-numeric id must degrade to the bearer-string match: persisting it
|
|
425
|
+
// would fail record validation and re-create the token on every read.
|
|
426
|
+
tokenId: Number.isFinite(response.token_id)
|
|
427
|
+
? response.token_id
|
|
428
|
+
: undefined,
|
|
316
429
|
}
|
|
317
430
|
await this.set(address, token.accountIndex, token)
|
|
318
431
|
|
|
@@ -327,6 +440,57 @@ export class LighterReadOnlyTokenManager {
|
|
|
327
440
|
},
|
|
328
441
|
}
|
|
329
442
|
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Revoke the registry rows this SDK owns and no longer needs. Lighter's
|
|
446
|
+
* `tokens/create` appends a row on every call, so without this pass the
|
|
447
|
+
* user's registry grows on every rotation.
|
|
448
|
+
*/
|
|
449
|
+
private async revokeStaleOwnTokens(
|
|
450
|
+
authorization: string,
|
|
451
|
+
address: Address,
|
|
452
|
+
accountIndex: number
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
try {
|
|
455
|
+
const live = await this.readStoredToken(address, accountIndex)
|
|
456
|
+
const { api_tokens: rows } = await this.listFetcher({
|
|
457
|
+
url: `${this.lighterApiUrl}/api/v1/tokens`,
|
|
458
|
+
authorization,
|
|
459
|
+
accountIndex,
|
|
460
|
+
})
|
|
461
|
+
const nowSeconds = Math.floor(this.now() / 1000)
|
|
462
|
+
const stale = rows
|
|
463
|
+
// A row is ours only when its name matches the name we create rows
|
|
464
|
+
// under. Any other name belongs to the user and must survive.
|
|
465
|
+
.filter((row) => row.name === DEFAULT_READ_ONLY_TOKEN_NAME)
|
|
466
|
+
.filter((row) => !row.revoked)
|
|
467
|
+
// Keep the row the local store holds; with no stored record every owned
|
|
468
|
+
// row is stale, and a peer client's row self-heals via `retryOnRevoked`.
|
|
469
|
+
.filter((row) => row.expiry <= nowSeconds || !isLiveRow(row, live))
|
|
470
|
+
.sort((a, b) => a.token_id - b.token_id)
|
|
471
|
+
|
|
472
|
+
for (const row of stale) {
|
|
473
|
+
try {
|
|
474
|
+
await this.revokeFetcher({
|
|
475
|
+
url: `${this.lighterApiUrl}/api/v1/tokens/revoke`,
|
|
476
|
+
authorization,
|
|
477
|
+
tokenId: row.token_id,
|
|
478
|
+
accountIndex,
|
|
479
|
+
})
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.warn(
|
|
482
|
+
`[lighter] could not revoke stale read-only token ${row.token_id}; continuing.`,
|
|
483
|
+
err
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.warn(
|
|
489
|
+
'[lighter] could not clean up stale read-only tokens; creating the new one anyway.',
|
|
490
|
+
err
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
330
494
|
}
|
|
331
495
|
|
|
332
496
|
/**
|
|
@@ -334,7 +498,7 @@ export class LighterReadOnlyTokenManager {
|
|
|
334
498
|
* `tokens/create` endpoint and returns the parsed response. Throws a
|
|
335
499
|
* {@link PerpsError} with the Lighter-side body when the response is
|
|
336
500
|
* non-2xx.
|
|
337
|
-
* @
|
|
501
|
+
* @internal
|
|
338
502
|
*/
|
|
339
503
|
export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
|
|
340
504
|
url,
|
|
@@ -368,3 +532,66 @@ export const defaultLighterTokenFetcher: LighterTokenFetcher = async ({
|
|
|
368
532
|
|
|
369
533
|
return (await response.json()) as LighterCreateTokenResponse
|
|
370
534
|
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Default list fetcher: `GET /api/v1/tokens` with `account_index` as a query
|
|
538
|
+
* parameter. Lighter's OpenAPI defines no request body for this route. Throws
|
|
539
|
+
* a {@link PerpsError} with the Lighter-side body when the response is
|
|
540
|
+
* non-2xx.
|
|
541
|
+
* @internal
|
|
542
|
+
*/
|
|
543
|
+
export const defaultLighterTokenListFetcher: LighterTokenListFetcher = async ({
|
|
544
|
+
url,
|
|
545
|
+
authorization,
|
|
546
|
+
accountIndex,
|
|
547
|
+
}) => {
|
|
548
|
+
const query = new URLSearchParams({ account_index: String(accountIndex) })
|
|
549
|
+
|
|
550
|
+
const response = await fetch(`${url}?${query.toString()}`, {
|
|
551
|
+
headers: { authorization },
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
if (!response.ok) {
|
|
555
|
+
const body = await response.text()
|
|
556
|
+
throw new PerpsError(
|
|
557
|
+
PerpsErrorCode.ServerError,
|
|
558
|
+
`Lighter tokens returned ${response.status}: ${body || '<empty>'}`
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return (await response.json()) as LighterListTokensResponse
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Default revoke fetcher: posts `application/x-www-form-urlencoded` to
|
|
567
|
+
* Lighter's `tokens/revoke` endpoint, which is the content type Lighter's
|
|
568
|
+
* OpenAPI declares for that route. Throws a {@link PerpsError} with the
|
|
569
|
+
* Lighter-side body when the response is non-2xx.
|
|
570
|
+
* @internal
|
|
571
|
+
*/
|
|
572
|
+
export const defaultLighterTokenRevokeFetcher: LighterTokenRevokeFetcher =
|
|
573
|
+
async ({ url, authorization, tokenId, accountIndex }) => {
|
|
574
|
+
const form = new URLSearchParams({
|
|
575
|
+
token_id: String(tokenId),
|
|
576
|
+
account_index: String(accountIndex),
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
const response = await fetch(url, {
|
|
580
|
+
method: 'POST',
|
|
581
|
+
headers: {
|
|
582
|
+
authorization,
|
|
583
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
584
|
+
},
|
|
585
|
+
body: form,
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
if (!response.ok) {
|
|
589
|
+
const body = await response.text()
|
|
590
|
+
throw new PerpsError(
|
|
591
|
+
PerpsErrorCode.ServerError,
|
|
592
|
+
`Lighter tokens/revoke returned ${response.status}: ${body || '<empty>'}`
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return (await response.json()) as LighterRevokeTokenResponse
|
|
597
|
+
}
|
|
@@ -64,7 +64,7 @@ export interface LighterSignActionsDeps {
|
|
|
64
64
|
const TOKEN_AUTH_MUTATION_KINDS = new Set(['changeAccountTier', 'referralUse'])
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Lifetime of the per-call auth token
|
|
67
|
+
* Lifetime of the per-call auth token issued for a token-authenticated venue
|
|
68
68
|
* mutation. Minutes, not the read endpoints' hours: it authenticates one POST
|
|
69
69
|
* that completes immediately, so it never lingers usable.
|
|
70
70
|
*/
|
|
@@ -133,11 +133,19 @@ export const createLighterApiKeyFreshness = (): LighterApiKeyFreshness => {
|
|
|
133
133
|
checkedAtMs.set(windowKey, Date.now())
|
|
134
134
|
return
|
|
135
135
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
// An answered read that names no entry is a verdict, not a gap: the slot
|
|
137
|
+
// holds no key the stored one can match, so a signature it produces
|
|
138
|
+
// cannot verify.
|
|
139
|
+
if (registered === undefined) {
|
|
140
|
+
throw new PerpsError(
|
|
141
|
+
PerpsErrorCode.SDKError,
|
|
142
|
+
`The Lighter API key stored for ${address} is not registered: ` +
|
|
143
|
+
`account ${apiKey.accountIndex} holds no key in API-key slot ` +
|
|
144
|
+
`${apiKey.apiKeyIndex}. Run prepareAccount / REGISTER_API_KEY to ` +
|
|
145
|
+
'register a fresh key.'
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
if (normalizeLighterPublicKey(registered.public_key) !== storedKey) {
|
|
141
149
|
throw new PerpsError(
|
|
142
150
|
PerpsErrorCode.SDKError,
|
|
143
151
|
`The Lighter API key stored for ${address} is no longer registered: ` +
|
|
@@ -146,7 +154,7 @@ export const createLighterApiKeyFreshness = (): LighterApiKeyFreshness => {
|
|
|
146
154
|
)
|
|
147
155
|
}
|
|
148
156
|
// Every read the venue answered counts against the window, so a burst
|
|
149
|
-
// costs one read whatever the verdict. A
|
|
157
|
+
// costs one read whatever the verdict. A rejection throws above, so it
|
|
150
158
|
// never records and the next batch reads again.
|
|
151
159
|
checkedAtMs.set(windowKey, Date.now())
|
|
152
160
|
},
|
|
@@ -295,13 +303,12 @@ async function signRegisterApiKey(
|
|
|
295
303
|
l1Signature
|
|
296
304
|
)
|
|
297
305
|
|
|
298
|
-
|
|
306
|
+
await deps.keyStore.set(address, {
|
|
299
307
|
accountIndex,
|
|
300
308
|
apiKeyIndex,
|
|
301
309
|
apiKeyPrivateKey: keypair.privateKey,
|
|
302
310
|
apiKeyPublicKey: keypair.publicKey,
|
|
303
|
-
}
|
|
304
|
-
await deps.keyStore.set(address, apiKey)
|
|
311
|
+
})
|
|
305
312
|
|
|
306
313
|
return {
|
|
307
314
|
action: step.action,
|
|
@@ -431,7 +438,7 @@ async function signTransfer(
|
|
|
431
438
|
*
|
|
432
439
|
* Neither endpoint consumes a wasm-signed transaction — both authenticate with
|
|
433
440
|
* a Lighter auth token and enforce their business rules (open positions,
|
|
434
|
-
* pending orders, 24h tier cooldown) server-side. We
|
|
441
|
+
* pending orders, 24h tier cooldown) server-side. We create a fresh short-lived
|
|
435
442
|
* token per call (never the stored read-only token, never persisted) and POST
|
|
436
443
|
* the mirror of the backend's form body directly to Lighter, so no auth token
|
|
437
444
|
* ever transits the LI.FI backend. A non-success verdict surfaces Lighter's
|
package/src/types/fill.ts
CHANGED
|
@@ -48,6 +48,12 @@ export type LtTrade = {
|
|
|
48
48
|
// trade rows that predate the field.
|
|
49
49
|
taker_entry_quote_before?: string
|
|
50
50
|
maker_entry_quote_before?: string
|
|
51
|
+
// Per-counterparty initial-margin-fraction snapshot BEFORE the trade: the
|
|
52
|
+
// leverage that counterparty had set on the market, as an integer percent on
|
|
53
|
+
// `LIGHTER_IMF_PERCENT_SCALE` (`500` = 5.00% = 20x). Optional: absent on
|
|
54
|
+
// older trade rows that predate the field.
|
|
55
|
+
taker_initial_margin_fraction_before?: number
|
|
56
|
+
maker_initial_margin_fraction_before?: number
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
/**
|
package/src/utils/mapFill.ts
CHANGED
|
@@ -11,8 +11,12 @@ import {
|
|
|
11
11
|
OrderType,
|
|
12
12
|
} from '@lifi/perps-types'
|
|
13
13
|
import Big from 'big.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
LIGHTER_FEE_TICK_SCALE,
|
|
16
|
+
LIGHTER_IMF_PERCENT_SCALE,
|
|
17
|
+
} from '../constants.js'
|
|
15
18
|
import type { LtTrade } from '../types/index.js'
|
|
19
|
+
import { leverageFromImf } from './mapPosition.js'
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Fee charged on a fill, in the market's quote asset. Lighter publishes the
|
|
@@ -35,6 +39,18 @@ const tickToFeeAmount = (
|
|
|
35
39
|
.div(LIGHTER_FEE_TICK_SCALE)
|
|
36
40
|
.toFixed()
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Display leverage the viewer had set on the market when the trade executed.
|
|
44
|
+
* Lighter reports it as the pre-trade initial margin fraction, an integer
|
|
45
|
+
* percent on `LIGHTER_IMF_PERCENT_SCALE`: `500` is 5.00%, so 20x. Returns
|
|
46
|
+
* `undefined` when the row omits the fraction. Lighter declares the fraction a
|
|
47
|
+
* `StrictInt`, so the scale division is exact and `toFixed()` loses nothing.
|
|
48
|
+
*/
|
|
49
|
+
const leverageFromTradeImf = (imf: number | undefined): number | undefined =>
|
|
50
|
+
imf === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: leverageFromImf(new Big(imf).div(LIGHTER_IMF_PERCENT_SCALE).toFixed())
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* Realized PnL on a position-reducing fill, derived from the pre-trade entry
|
|
40
56
|
* basis. Returns `undefined` for opens/increases (nothing closes) or when the
|
|
@@ -101,6 +117,9 @@ export const mapFill = (
|
|
|
101
117
|
const integratorFeeTick = isMaker
|
|
102
118
|
? trade.integrator_maker_fee
|
|
103
119
|
: trade.integrator_taker_fee
|
|
120
|
+
const imfBefore = isMaker
|
|
121
|
+
? trade.maker_initial_margin_fraction_before
|
|
122
|
+
: trade.taker_initial_margin_fraction_before
|
|
104
123
|
|
|
105
124
|
return {
|
|
106
125
|
id: trade.trade_id.toString(),
|
|
@@ -124,6 +143,7 @@ export const mapFill = (
|
|
|
124
143
|
),
|
|
125
144
|
asset: market.quoteAsset.displaySymbol,
|
|
126
145
|
},
|
|
146
|
+
leverage: leverageFromTradeImf(imfBefore),
|
|
127
147
|
realizedPnl: deriveRealizedPnl(
|
|
128
148
|
startPosition,
|
|
129
149
|
entryQuoteBefore,
|