@lifi/perps-sdk-provider-lighter 13.0.0 → 13.0.2
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 +6 -8
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/signers/signActions.d.ts +6 -1
- package/dist/cjs/signers/signActions.d.ts.map +1 -1
- package/dist/cjs/signers/signActions.js +49 -28
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/signers/wasmLoader.d.ts +0 -3
- package/dist/cjs/signers/wasmLoader.d.ts.map +1 -1
- package/dist/cjs/signers/wasmLoader.js +3 -2
- package/dist/cjs/signers/wasmLoader.js.map +1 -1
- package/dist/cjs/utils/registeredApiKey.d.ts +8 -0
- package/dist/cjs/utils/registeredApiKey.d.ts.map +1 -0
- package/dist/cjs/utils/registeredApiKey.js +11 -0
- package/dist/cjs/utils/registeredApiKey.js.map +1 -0
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +4 -7
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/signers/signActions.d.ts +22 -1
- package/dist/esm/signers/signActions.d.ts.map +1 -1
- package/dist/esm/signers/signActions.js +90 -47
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/signers/wasmLoader.d.ts +0 -3
- package/dist/esm/signers/wasmLoader.d.ts.map +1 -1
- package/dist/esm/signers/wasmLoader.js +7 -2
- package/dist/esm/signers/wasmLoader.js.map +1 -1
- package/dist/esm/utils/registeredApiKey.d.ts +21 -0
- package/dist/esm/utils/registeredApiKey.d.ts.map +1 -0
- package/dist/esm/utils/registeredApiKey.js +13 -0
- package/dist/esm/utils/registeredApiKey.js.map +1 -0
- package/dist/types/LighterProvider.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts +22 -1
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/signers/wasmLoader.d.ts +0 -3
- package/dist/types/signers/wasmLoader.d.ts.map +1 -1
- package/dist/types/utils/registeredApiKey.d.ts +21 -0
- package/dist/types/utils/registeredApiKey.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/LighterProvider.ts +10 -17
- package/src/signers/signActions.ts +169 -58
- package/src/signers/wasmLoader.ts +7 -3
- package/src/utils/registeredApiKey.ts +34 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signActions.d.ts","sourceRoot":"","sources":["../../../src/signers/signActions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAA8B,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAOnC,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"signActions.d.ts","sourceRoot":"","sources":["../../../src/signers/signActions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAA8B,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAOnC,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAMxE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC1E,OAAO,KAAK,EAEV,aAAa,EAEd,MAAM,oBAAoB,CAAA;AAE3B;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,aAAa,CAAA;IACrB,QAAQ,EAAE,eAAe,CAAA;IACzB;;qCAEiC;IACjC,SAAS,EAAE,gBAAgB,CAAA;IAC3B;;;OAGG;IACH,eAAe,EAAE,sBAAsB,CAAA;IACvC;;;;;OAKG;IACH,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACvD;AAyBD;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,qBAAqB,CACnB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,QAAO,sBAqD/C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,sBAAsB,EAC5B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,kBAAkB,EAAE,EAC3B,GAAG,EAAE,kBAAkB,GAAG,SAAS,GAClC,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAkCrC;AAgUD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,EAAE,EACxB,GAAG,EAAE,kBAAkB,GAAG,SAAS,GAClC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CA6ElC;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,sBAAsB,EAC5B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,kBAAkB,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsB7B"}
|
|
@@ -14,9 +14,6 @@ export interface LighterWasmExports {
|
|
|
14
14
|
CreateClient: (url: string, privateKey: string, chainId: number, apiKeyIndex: number, accountIndex: number) => {
|
|
15
15
|
error?: string;
|
|
16
16
|
};
|
|
17
|
-
CheckClient: (apiKeyIndex: number, accountIndex: number) => {
|
|
18
|
-
error?: string;
|
|
19
|
-
};
|
|
20
17
|
/**
|
|
21
18
|
* Returns `{authToken}` on success — note the field name is `authToken`,
|
|
22
19
|
* NOT `token`. Reading the wrong field silently fails (Go sets nothing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasmLoader.d.ts","sourceRoot":"","sources":["../../../src/signers/wasmLoader.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,KACjB;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvB
|
|
1
|
+
{"version":3,"file":"wasmLoader.d.ts","sourceRoot":"","sources":["../../../src/signers/wasmLoader.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,KACjB;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvB;;;;;OAKG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,KACjB;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,KACjB,UAAU,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C;;;;;;;;;;;;;;;OAeG;IACH,eAAe,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACnD,2EAA2E;IAC3E,eAAe,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACnD;;;OAGG;IACH,mBAAmB,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACvD;;;;;;;;OAQG;IACH,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7E,iFAAiF;IACjF,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IAChD;;;OAGG;IACH,kBAAkB,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACtD;;;;;OAKG;IACH,eAAe,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACnD;;;OAGG;IACH,gBAAgB,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IACpD;;;;;;;OAOG;IACH,qBAAqB,EAAE,CACrB,GAAG,IAAI,EAAE,OAAO,EAAE,KACf,UAAU,GAAG;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C;;;;OAIG;IACH,uBAAuB,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;IAC3D;;;;OAIG;IACH,4BAA4B,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,UAAU,CAAA;CACjE;AAED,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAuHD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAYnE;AAyDD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LighterApiClient } from './apiClient.js';
|
|
2
|
+
/**
|
|
3
|
+
* One entry of Lighter's `/api/v1/apikeys` response: the public key the venue
|
|
4
|
+
* holds in an API-key slot.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface LighterRegisteredApiKey {
|
|
9
|
+
api_key_index: number;
|
|
10
|
+
public_key: string;
|
|
11
|
+
}
|
|
12
|
+
/** Compare Lighter public keys irrespective of `0x` prefix / casing. */
|
|
13
|
+
export declare const normalizeLighterPublicKey: (key: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Read the public key Lighter holds in one API-key slot. Returns `undefined`
|
|
16
|
+
* when the response carries no entry for `apiKeyIndex`.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare const fetchRegisteredApiKey: (client: LighterApiClient, accountIndex: number, apiKeyIndex: number) => Promise<LighterRegisteredApiKey | undefined>;
|
|
21
|
+
//# sourceMappingURL=registeredApiKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registeredApiKey.d.ts","sourceRoot":"","sources":["../../../src/utils/registeredApiKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,GAAI,KAAK,MAAM,KAAG,MACjB,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,gBAAgB,EACxB,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAM7C,CAAA"}
|
package/package.json
CHANGED
package/src/LighterProvider.ts
CHANGED
|
@@ -76,7 +76,10 @@ import { createAuthToken } from './signers/createAuthToken.js'
|
|
|
76
76
|
import { LighterKeyStore } from './signers/LighterKeyStore.js'
|
|
77
77
|
import { LighterReadOnlyTokenManager } from './signers/LighterReadOnlyTokenManager.js'
|
|
78
78
|
import { LighterSigner } from './signers/LighterSigner.js'
|
|
79
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
createLighterApiKeyFreshness,
|
|
81
|
+
lighterSignActions,
|
|
82
|
+
} from './signers/signActions.js'
|
|
80
83
|
import type {
|
|
81
84
|
LtAccountLimits,
|
|
82
85
|
LtDepositHistoryResponse,
|
|
@@ -119,6 +122,10 @@ import {
|
|
|
119
122
|
toRequiredBig,
|
|
120
123
|
} from './utils/index.js'
|
|
121
124
|
import { mapRunningTwap } from './utils/mapTwap.js'
|
|
125
|
+
import {
|
|
126
|
+
fetchRegisteredApiKey,
|
|
127
|
+
normalizeLighterPublicKey,
|
|
128
|
+
} from './utils/registeredApiKey.js'
|
|
122
129
|
|
|
123
130
|
const ZERO_FEE_TIER = { maker: '0', taker: '0' }
|
|
124
131
|
|
|
@@ -170,10 +177,6 @@ interface ReadOnlyCreationBackoff {
|
|
|
170
177
|
attempt: number
|
|
171
178
|
}
|
|
172
179
|
|
|
173
|
-
/** Compare Lighter public keys irrespective of `0x` prefix / casing. */
|
|
174
|
-
const normalizeLighterPublicKey = (key: string): string =>
|
|
175
|
-
key.replace(/^0x/i, '').toLowerCase()
|
|
176
|
-
|
|
177
180
|
/**
|
|
178
181
|
* Consumer-level overrides for a ready-made Lighter provider plugin. Every
|
|
179
182
|
* deployment fact — provider key, endpoints, signing chain id, collateral
|
|
@@ -285,6 +288,7 @@ export const createLighterProvider = (
|
|
|
285
288
|
collateralAssetIndex: collateral.assetIndex,
|
|
286
289
|
})
|
|
287
290
|
const keyStore = new LighterKeyStore(storage, providerKey)
|
|
291
|
+
const apiKeyFreshness = createLighterApiKeyFreshness()
|
|
288
292
|
const readOnlyTokenManager = new LighterReadOnlyTokenManager({
|
|
289
293
|
storage,
|
|
290
294
|
providerKey,
|
|
@@ -518,18 +522,6 @@ export const createLighterProvider = (
|
|
|
518
522
|
}
|
|
519
523
|
}
|
|
520
524
|
|
|
521
|
-
const fetchRegisteredApiKey = async (
|
|
522
|
-
client: LighterApiClient,
|
|
523
|
-
accountIndex: number,
|
|
524
|
-
apiKeyIndex: number
|
|
525
|
-
): Promise<{ api_key_index: number; public_key: string } | undefined> => {
|
|
526
|
-
const response = await client.get<{
|
|
527
|
-
code: number
|
|
528
|
-
api_keys: Array<{ api_key_index: number; public_key: string }>
|
|
529
|
-
}>('/api/v1/apikeys', { account_index: accountIndex })
|
|
530
|
-
return response.api_keys?.find((k) => k.api_key_index === apiKeyIndex)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
525
|
const fetchAccountLimits = (
|
|
534
526
|
client: LighterApiClient,
|
|
535
527
|
accountIndex: number,
|
|
@@ -1449,6 +1441,7 @@ export const createLighterProvider = (
|
|
|
1449
1441
|
signer,
|
|
1450
1442
|
keyStore,
|
|
1451
1443
|
apiClient: apiClient(),
|
|
1444
|
+
apiKeyFreshness,
|
|
1452
1445
|
resolveAccountIndex: async (addr) => {
|
|
1453
1446
|
const apiKey = await keyStore.get(addr)
|
|
1454
1447
|
if (apiKey !== null) {
|
|
@@ -16,8 +16,17 @@ import {
|
|
|
16
16
|
LIGHTER_MUTATION_SUCCESS_CODE,
|
|
17
17
|
} from '../constants.js'
|
|
18
18
|
import type { ApiParams, LighterApiClient } from '../utils/apiClient.js'
|
|
19
|
+
import {
|
|
20
|
+
fetchRegisteredApiKey,
|
|
21
|
+
type LighterRegisteredApiKey,
|
|
22
|
+
normalizeLighterPublicKey,
|
|
23
|
+
} from '../utils/registeredApiKey.js'
|
|
19
24
|
import type { LighterApiKey, LighterKeyStore } from './LighterKeyStore.js'
|
|
20
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
LighterSignedBlob,
|
|
27
|
+
LighterSigner,
|
|
28
|
+
LighterSignerContext,
|
|
29
|
+
} from './LighterSigner.js'
|
|
21
30
|
|
|
22
31
|
/**
|
|
23
32
|
* Per-batch dependencies the Lighter `signActions` implementation needs:
|
|
@@ -35,6 +44,11 @@ export interface LighterSignActionsDeps {
|
|
|
35
44
|
* `changeAccountTier` / `referral/use` POSTs so their auth token never
|
|
36
45
|
* transits the LI.FI backend. */
|
|
37
46
|
apiClient: LighterApiClient
|
|
47
|
+
/**
|
|
48
|
+
* Pre-sign guard that the stored API key still occupies its venue slot.
|
|
49
|
+
* Created once per provider instance so its freshness window spans batches.
|
|
50
|
+
*/
|
|
51
|
+
apiKeyFreshness: LighterApiKeyFreshness
|
|
38
52
|
/**
|
|
39
53
|
* Resolve the user's Lighter `accountIndex` for an L1 address. Required
|
|
40
54
|
* by REGISTER_API_KEY so the ChangePubKey blob carries the right
|
|
@@ -59,6 +73,89 @@ const TOKEN_AUTH_MUTATION_KINDS = new Set(['changeAccountTier', 'referralUse'])
|
|
|
59
73
|
*/
|
|
60
74
|
const TOKEN_AUTH_MUTATION_DEADLINE_SECONDS = 5 * 60
|
|
61
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Window in which one `/apikeys` read stands for the next. 30s keeps a burst
|
|
78
|
+
* of batches — several orders submitted back to back, a close-then-reduce
|
|
79
|
+
* sequence — at one read, and still surfaces a key rotated on another device
|
|
80
|
+
* on the user's next action instead of after a venue rejection.
|
|
81
|
+
*/
|
|
82
|
+
const API_KEY_FRESHNESS_WINDOW_MS = 30_000
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pre-sign guard that the stored Lighter API key is still the key registered
|
|
86
|
+
* in its venue slot. Owns the freshness window, so one instance is created per
|
|
87
|
+
* provider instance and shared by every batch that instance signs.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
export interface LighterApiKeyFreshness {
|
|
92
|
+
assertStillRegistered(
|
|
93
|
+
client: LighterApiClient,
|
|
94
|
+
address: Address,
|
|
95
|
+
apiKey: LighterApiKey
|
|
96
|
+
): Promise<void>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the guard {@link LighterSignActionsDeps.apiKeyFreshness} needs.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
export const createLighterApiKeyFreshness = (): LighterApiKeyFreshness => {
|
|
105
|
+
const checkedAtMs = new Map<string, number>()
|
|
106
|
+
return {
|
|
107
|
+
async assertStillRegistered(client, address, apiKey) {
|
|
108
|
+
const storedKey = normalizeLighterPublicKey(apiKey.apiKeyPublicKey)
|
|
109
|
+
// Keyed on the stored key, so a rotation retires the entry by itself.
|
|
110
|
+
const windowKey = `${apiKey.accountIndex}:${apiKey.apiKeyIndex}:${storedKey}`
|
|
111
|
+
const checkedAt = checkedAtMs.get(windowKey)
|
|
112
|
+
if (
|
|
113
|
+
checkedAt !== undefined &&
|
|
114
|
+
Date.now() - checkedAt < API_KEY_FRESHNESS_WINDOW_MS
|
|
115
|
+
) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let registered: LighterRegisteredApiKey | undefined
|
|
120
|
+
try {
|
|
121
|
+
registered = await fetchRegisteredApiKey(
|
|
122
|
+
client,
|
|
123
|
+
apiKey.accountIndex,
|
|
124
|
+
apiKey.apiKeyIndex
|
|
125
|
+
)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// A transport failure or a non-success body proves nothing about the
|
|
128
|
+
// slot, so the venue decides. A failed check never blocks a signature.
|
|
129
|
+
// The code names the failure without the response body, which lists
|
|
130
|
+
// the account's other key slots.
|
|
131
|
+
console.warn(
|
|
132
|
+
'[lighter] could not read the registered API key before signing; ' +
|
|
133
|
+
'signing anyway and letting the venue decide.',
|
|
134
|
+
err instanceof PerpsError ? err.code : String(err)
|
|
135
|
+
)
|
|
136
|
+
checkedAtMs.set(windowKey, Date.now())
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
// A slot carrying no entry leaves nothing to compare against.
|
|
140
|
+
if (
|
|
141
|
+
registered !== undefined &&
|
|
142
|
+
normalizeLighterPublicKey(registered.public_key) !== storedKey
|
|
143
|
+
) {
|
|
144
|
+
throw new PerpsError(
|
|
145
|
+
PerpsErrorCode.SDKError,
|
|
146
|
+
`The Lighter API key stored for ${address} is no longer registered: ` +
|
|
147
|
+
`API-key slot ${apiKey.apiKeyIndex} now holds a different key. ` +
|
|
148
|
+
'Run prepareAccount / REGISTER_API_KEY to register a fresh key.'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
// Every read the venue answered counts against the window, so a burst
|
|
152
|
+
// costs one read whatever the verdict. A mismatch throws above, so it
|
|
153
|
+
// never records and the next batch reads again.
|
|
154
|
+
checkedAtMs.set(windowKey, Date.now())
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
62
159
|
/**
|
|
63
160
|
* Sign a `WASM_BLOB` batch (Lighter). Ensures the user's API keypair is
|
|
64
161
|
* registered first — generating one and running the REGISTER_API_KEY
|
|
@@ -78,6 +175,22 @@ export async function signWasmBlobActions(
|
|
|
78
175
|
steps: WasmBlobActionStep[],
|
|
79
176
|
ctx: SignActionsContext | undefined
|
|
80
177
|
): Promise<WasmBlobSignedActionStep[]> {
|
|
178
|
+
// REGISTER_API_KEY replaces the slot's key, so for such a batch the stored
|
|
179
|
+
// key is expected to differ from the registered one.
|
|
180
|
+
const replacesApiKey = steps.some(
|
|
181
|
+
(step) => step.action === ActionType.REGISTER_API_KEY
|
|
182
|
+
)
|
|
183
|
+
if (!replacesApiKey) {
|
|
184
|
+
const storedKey = await deps.keyStore.get(address)
|
|
185
|
+
if (storedKey !== null) {
|
|
186
|
+
await deps.apiKeyFreshness.assertStillRegistered(
|
|
187
|
+
deps.apiClient,
|
|
188
|
+
address,
|
|
189
|
+
storedKey
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
81
194
|
const signed: WasmBlobSignedActionStep[] = []
|
|
82
195
|
for (const step of steps) {
|
|
83
196
|
if (step.action === ActionType.REGISTER_API_KEY) {
|
|
@@ -202,34 +315,41 @@ async function signRegisterApiKey(
|
|
|
202
315
|
}
|
|
203
316
|
|
|
204
317
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* 3.
|
|
210
|
-
*
|
|
318
|
+
* Shared L1-countersign flow for the Lighter WASM_BLOB actions that require
|
|
319
|
+
* the end-user's Ethereum wallet:
|
|
320
|
+
* 1. Guard that the caller supplied `ctx.userWallet`.
|
|
321
|
+
* 2. Load the user's stored API key (no keypair generation).
|
|
322
|
+
* 3. Wasm-sign the blob via `signMethod`, obtaining the EIP-191 L1 message
|
|
323
|
+
* the wallet must countersign.
|
|
324
|
+
* 4. Have the user's L1 Ethereum wallet sign that message.
|
|
325
|
+
* 5. Inject the L1 signature into the txInfo JSON as `L1Sig`.
|
|
211
326
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
327
|
+
* `actionLabel` names the caller's action in the missing-wallet error
|
|
328
|
+
* message. `signMethod` is the caller's wasm-signing call; each caller keeps
|
|
329
|
+
* its own doc comment stating why its flow needs the L1 signature.
|
|
215
330
|
*/
|
|
216
|
-
async function
|
|
331
|
+
async function signL1CountersignedWasmAction(
|
|
217
332
|
deps: LighterSignActionsDeps,
|
|
218
333
|
address: Address,
|
|
219
334
|
step: WasmBlobActionStep,
|
|
220
|
-
ctx: SignActionsContext | undefined
|
|
335
|
+
ctx: SignActionsContext | undefined,
|
|
336
|
+
actionLabel: 'APPROVE_INTEGRATOR' | 'TRANSFER',
|
|
337
|
+
signMethod: (
|
|
338
|
+
wasmSignParams: Record<string, unknown>,
|
|
339
|
+
context: LighterSignerContext
|
|
340
|
+
) => Promise<LighterSignedBlob & { messageToSign: string }>
|
|
221
341
|
): Promise<WasmBlobSignedActionStep> {
|
|
222
342
|
const walletSigner = ctx?.userWallet
|
|
223
343
|
if (!walletSigner) {
|
|
224
344
|
throw new PerpsError(
|
|
225
345
|
PerpsErrorCode.SDKError,
|
|
226
|
-
|
|
346
|
+
`${actionLabel} requires the end-user wallet — pass \`userWallet\` to ` +
|
|
227
347
|
'createPerpsClient or call setUserWallet(walletClient).'
|
|
228
348
|
)
|
|
229
349
|
}
|
|
230
350
|
|
|
231
351
|
const apiKey = await requireApiKey(deps, address)
|
|
232
|
-
const signed = await
|
|
352
|
+
const signed = await signMethod(step.wasmSignParams, {
|
|
233
353
|
apiKeyPrivateKey: apiKey.apiKeyPrivateKey,
|
|
234
354
|
apiKeyIndex: apiKey.apiKeyIndex,
|
|
235
355
|
accountIndex: apiKey.accountIndex,
|
|
@@ -257,58 +377,49 @@ async function signApproveIntegrator(
|
|
|
257
377
|
}
|
|
258
378
|
|
|
259
379
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* 3. Have the user's L1 Ethereum wallet sign that message.
|
|
265
|
-
* 4. Inject the L1 signature into the txInfo JSON as `L1Sig`.
|
|
266
|
-
*
|
|
267
|
-
* Requires the end-user's wallet in `ctx.userWallet`: the destination account
|
|
268
|
-
* and the amount are only bound to the account owner by the user's `L1Sig`,
|
|
269
|
-
* so the stored API key alone must not authorize the move.
|
|
380
|
+
* APPROVE_INTEGRATOR flow. Requires the end-user's wallet in
|
|
381
|
+
* `ctx.userWallet`: Lighter rejects the tx without the user's `L1Sig` when
|
|
382
|
+
* the integrator account belongs to a different L1 address with non-zero fee
|
|
383
|
+
* caps.
|
|
270
384
|
*/
|
|
271
|
-
async function
|
|
385
|
+
async function signApproveIntegrator(
|
|
272
386
|
deps: LighterSignActionsDeps,
|
|
273
387
|
address: Address,
|
|
274
388
|
step: WasmBlobActionStep,
|
|
275
389
|
ctx: SignActionsContext | undefined
|
|
276
390
|
): Promise<WasmBlobSignedActionStep> {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const apiKey = await requireApiKey(deps, address)
|
|
287
|
-
const signed = await deps.signer.signTransfer(step.wasmSignParams, {
|
|
288
|
-
apiKeyPrivateKey: apiKey.apiKeyPrivateKey,
|
|
289
|
-
apiKeyIndex: apiKey.apiKeyIndex,
|
|
290
|
-
accountIndex: apiKey.accountIndex,
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
const l1Signature = await walletSigner.signMessage({
|
|
294
|
-
account: walletSigner.account,
|
|
295
|
-
message: signed.messageToSign,
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
const txInfoWithL1Sig = deps.signer.embedL1Signature(
|
|
299
|
-
signed.txInfo,
|
|
300
|
-
l1Signature
|
|
391
|
+
return await signL1CountersignedWasmAction(
|
|
392
|
+
deps,
|
|
393
|
+
address,
|
|
394
|
+
step,
|
|
395
|
+
ctx,
|
|
396
|
+
'APPROVE_INTEGRATOR',
|
|
397
|
+
(wasmSignParams, context) =>
|
|
398
|
+
deps.signer.signApproveIntegrator(wasmSignParams, context)
|
|
301
399
|
)
|
|
400
|
+
}
|
|
302
401
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
402
|
+
/**
|
|
403
|
+
* TRANSFER flow. Requires the end-user's wallet in `ctx.userWallet`: the
|
|
404
|
+
* destination account and the amount are only bound to the account owner by
|
|
405
|
+
* the user's `L1Sig`, so the stored API key alone must not authorize the
|
|
406
|
+
* move.
|
|
407
|
+
*/
|
|
408
|
+
async function signTransfer(
|
|
409
|
+
deps: LighterSignActionsDeps,
|
|
410
|
+
address: Address,
|
|
411
|
+
step: WasmBlobActionStep,
|
|
412
|
+
ctx: SignActionsContext | undefined
|
|
413
|
+
): Promise<WasmBlobSignedActionStep> {
|
|
414
|
+
return await signL1CountersignedWasmAction(
|
|
415
|
+
deps,
|
|
416
|
+
address,
|
|
417
|
+
step,
|
|
418
|
+
ctx,
|
|
419
|
+
'TRANSFER',
|
|
420
|
+
(wasmSignParams, context) =>
|
|
421
|
+
deps.signer.signTransfer(wasmSignParams, context)
|
|
422
|
+
)
|
|
312
423
|
}
|
|
313
424
|
|
|
314
425
|
/**
|
|
@@ -27,7 +27,6 @@ export interface LighterWasmExports {
|
|
|
27
27
|
apiKeyIndex: number,
|
|
28
28
|
accountIndex: number
|
|
29
29
|
) => { error?: string }
|
|
30
|
-
CheckClient: (apiKeyIndex: number, accountIndex: number) => { error?: string }
|
|
31
30
|
/**
|
|
32
31
|
* Returns `{authToken}` on success — note the field name is `authToken`,
|
|
33
32
|
* NOT `token`. Reading the wrong field silently fails (Go sets nothing
|
|
@@ -143,7 +142,6 @@ export interface SignResult {
|
|
|
143
142
|
const WASM_FUNCTION_NAMES = [
|
|
144
143
|
'GenerateAPIKey',
|
|
145
144
|
'CreateClient',
|
|
146
|
-
'CheckClient',
|
|
147
145
|
'CreateAuthToken',
|
|
148
146
|
'SignChangePubKey',
|
|
149
147
|
'SignCreateOrder',
|
|
@@ -304,9 +302,14 @@ async function loadWasmUncached(): Promise<LighterWasmExports> {
|
|
|
304
302
|
// Wait microtasks to let Go's init() register JS-bound functions.
|
|
305
303
|
await yieldToGoRuntime()
|
|
306
304
|
|
|
305
|
+
// Go's main.go only calls `js.Global().Set(name, ...)`; it never reads those
|
|
306
|
+
// names back, so dispatch survives the deletion while the captured handle
|
|
307
|
+
// keeps working. Left installed, they let any same-origin script sign with
|
|
308
|
+
// the API key held inside the instance. Never restore them for convenience.
|
|
309
|
+
const globals = globalThis as Record<string, unknown>
|
|
307
310
|
const exports: Partial<LighterWasmExports> = {}
|
|
308
311
|
for (const name of WASM_FUNCTION_NAMES) {
|
|
309
|
-
const fn =
|
|
312
|
+
const fn = globals[name]
|
|
310
313
|
if (typeof fn !== 'function') {
|
|
311
314
|
throw new Error(
|
|
312
315
|
`Lighter WASM did not export expected function: ${name}. ` +
|
|
@@ -314,6 +317,7 @@ async function loadWasmUncached(): Promise<LighterWasmExports> {
|
|
|
314
317
|
)
|
|
315
318
|
}
|
|
316
319
|
;(exports as Record<string, unknown>)[name] = fn
|
|
320
|
+
delete globals[name]
|
|
317
321
|
}
|
|
318
322
|
|
|
319
323
|
return exports as LighterWasmExports
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LighterApiClient } from './apiClient.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One entry of Lighter's `/api/v1/apikeys` response: the public key the venue
|
|
5
|
+
* holds in an API-key slot.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export interface LighterRegisteredApiKey {
|
|
10
|
+
api_key_index: number
|
|
11
|
+
public_key: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Compare Lighter public keys irrespective of `0x` prefix / casing. */
|
|
15
|
+
export const normalizeLighterPublicKey = (key: string): string =>
|
|
16
|
+
key.replace(/^0x/i, '').toLowerCase()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the public key Lighter holds in one API-key slot. Returns `undefined`
|
|
20
|
+
* when the response carries no entry for `apiKeyIndex`.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export const fetchRegisteredApiKey = async (
|
|
25
|
+
client: LighterApiClient,
|
|
26
|
+
accountIndex: number,
|
|
27
|
+
apiKeyIndex: number
|
|
28
|
+
): Promise<LighterRegisteredApiKey | undefined> => {
|
|
29
|
+
const response = await client.get<{
|
|
30
|
+
code: number
|
|
31
|
+
api_keys: LighterRegisteredApiKey[]
|
|
32
|
+
}>('/api/v1/apikeys', { account_index: accountIndex })
|
|
33
|
+
return response.api_keys?.find((k) => k.api_key_index === apiKeyIndex)
|
|
34
|
+
}
|