@lifi/perps-sdk-provider-lighter 1.6.0 → 1.8.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 +12 -0
- package/dist/cjs/LighterProvider.js.map +1 -1
- package/dist/cjs/accountConfig.d.ts.map +1 -1
- package/dist/cjs/accountConfig.js +16 -3
- 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/LighterSigner.d.ts.map +1 -1
- package/dist/cjs/signers/LighterSigner.js +12 -0
- package/dist/cjs/signers/LighterSigner.js.map +1 -1
- package/dist/cjs/signers/signActions.d.ts +2 -0
- package/dist/cjs/signers/signActions.d.ts.map +1 -1
- package/dist/cjs/signers/signActions.js +37 -19
- package/dist/cjs/signers/signActions.js.map +1 -1
- package/dist/cjs/signers/wasmLoader.d.ts +2 -0
- package/dist/cjs/signers/wasmLoader.d.ts.map +1 -1
- package/dist/cjs/signers/wasmLoader.js +2 -0
- package/dist/cjs/signers/wasmLoader.js.map +1 -1
- package/dist/cjs/types/account.d.ts +1 -0
- package/dist/cjs/types/account.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.d.ts +4 -0
- package/dist/cjs/utils/apiClient.d.ts.map +1 -1
- package/dist/cjs/utils/apiClient.js +15 -0
- package/dist/cjs/utils/apiClient.js.map +1 -1
- package/dist/cjs/utils/assetCollateral.d.ts +3 -0
- package/dist/cjs/utils/assetCollateral.d.ts.map +1 -0
- package/dist/cjs/utils/assetCollateral.js +13 -0
- package/dist/cjs/utils/assetCollateral.js.map +1 -0
- package/dist/esm/LighterProvider.d.ts.map +1 -1
- package/dist/esm/LighterProvider.js +12 -0
- package/dist/esm/LighterProvider.js.map +1 -1
- package/dist/esm/accountConfig.d.ts.map +1 -1
- package/dist/esm/accountConfig.js +28 -8
- package/dist/esm/accountConfig.js.map +1 -1
- package/dist/esm/constants.d.ts +9 -1
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +9 -1
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/signers/LighterSigner.d.ts.map +1 -1
- package/dist/esm/signers/LighterSigner.js +12 -0
- package/dist/esm/signers/LighterSigner.js.map +1 -1
- package/dist/esm/signers/signActions.d.ts +13 -5
- package/dist/esm/signers/signActions.d.ts.map +1 -1
- package/dist/esm/signers/signActions.js +68 -38
- package/dist/esm/signers/signActions.js.map +1 -1
- package/dist/esm/signers/wasmLoader.d.ts +12 -0
- package/dist/esm/signers/wasmLoader.d.ts.map +1 -1
- package/dist/esm/signers/wasmLoader.js +2 -0
- package/dist/esm/signers/wasmLoader.js.map +1 -1
- package/dist/esm/types/account.d.ts +6 -0
- package/dist/esm/types/account.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.d.ts +10 -0
- package/dist/esm/utils/apiClient.d.ts.map +1 -1
- package/dist/esm/utils/apiClient.js +21 -0
- package/dist/esm/utils/apiClient.js.map +1 -1
- package/dist/esm/utils/assetCollateral.d.ts +5 -0
- package/dist/esm/utils/assetCollateral.d.ts.map +1 -0
- package/dist/esm/utils/assetCollateral.js +15 -0
- package/dist/esm/utils/assetCollateral.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 +9 -1
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/signers/LighterSigner.d.ts.map +1 -1
- package/dist/types/signers/signActions.d.ts +13 -5
- package/dist/types/signers/signActions.d.ts.map +1 -1
- package/dist/types/signers/wasmLoader.d.ts +12 -0
- package/dist/types/signers/wasmLoader.d.ts.map +1 -1
- package/dist/types/types/account.d.ts +6 -0
- package/dist/types/types/account.d.ts.map +1 -1
- package/dist/types/utils/apiClient.d.ts +10 -0
- package/dist/types/utils/apiClient.d.ts.map +1 -1
- package/dist/types/utils/assetCollateral.d.ts +5 -0
- package/dist/types/utils/assetCollateral.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/LighterProvider.ts +15 -0
- package/src/accountConfig.ts +31 -8
- package/src/constants.ts +10 -0
- package/src/signers/LighterSigner.ts +28 -0
- package/src/signers/signActions.ts +112 -44
- package/src/signers/wasmLoader.ts +14 -0
- package/src/types/account.ts +6 -0
- package/src/utils/apiClient.ts +25 -0
- package/src/utils/assetCollateral.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifi/perps-sdk-provider-lighter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Lighter provider plugin for @lifi/perps-sdk",
|
|
5
5
|
"homepage": "https://github.com/lifinance/perps-sdk",
|
|
6
6
|
"bugs": {
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"big.js": "^7.0.1",
|
|
48
48
|
"viem": "^2.48.8",
|
|
49
|
-
"@lifi/perps-types": "^1.
|
|
49
|
+
"@lifi/perps-types": "^1.12.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@lifi/perps-sdk": "^1.
|
|
52
|
+
"@lifi/perps-sdk": "^1.6.0"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
package/src/LighterProvider.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
LighterApiClient,
|
|
82
82
|
LighterAuthRejectedError,
|
|
83
83
|
} from './utils/apiClient.js'
|
|
84
|
+
import { isAssetMarginEnabled } from './utils/assetCollateral.js'
|
|
84
85
|
import {
|
|
85
86
|
classifyAndMapOrders,
|
|
86
87
|
estimateLiquidationPrice,
|
|
@@ -697,12 +698,25 @@ export const lighterProvider = (
|
|
|
697
698
|
valueUsd: a.symbol === 'USDC' ? a.balance : '0',
|
|
698
699
|
}))
|
|
699
700
|
|
|
701
|
+
const assetCollateral = account.assets.flatMap((a) =>
|
|
702
|
+
a.margin_mode === undefined
|
|
703
|
+
? []
|
|
704
|
+
: [
|
|
705
|
+
{
|
|
706
|
+
assetId: String(a.asset_id),
|
|
707
|
+
enabled: isAssetMarginEnabled(a.margin_mode),
|
|
708
|
+
},
|
|
709
|
+
]
|
|
710
|
+
)
|
|
711
|
+
|
|
700
712
|
const config: LighterAccountConfig = {
|
|
701
713
|
provider: LIGHTER_PROVIDER_KEY,
|
|
702
714
|
accountIndex: account.index,
|
|
703
715
|
apiKeyIndex: DEFAULT_API_KEY_INDEX,
|
|
704
716
|
apiKeyRegistered,
|
|
705
717
|
accountType: account.account_type,
|
|
718
|
+
accountTradingMode: account.account_trading_mode,
|
|
719
|
+
assetCollateral,
|
|
706
720
|
// Satisfied when a non-expired read-only token is stored locally
|
|
707
721
|
// (`readOnlyTokenManager.get` filters out expired ones).
|
|
708
722
|
readOnlyTokenApproved: storedReadOnlyToken !== undefined,
|
|
@@ -1202,6 +1216,7 @@ export const lighterProvider = (
|
|
|
1202
1216
|
{
|
|
1203
1217
|
signer: signerRef,
|
|
1204
1218
|
keyStore: keyStoreRef,
|
|
1219
|
+
apiClient: apiClient(),
|
|
1205
1220
|
resolveAccountIndex: async (addr) => {
|
|
1206
1221
|
const apiKey = await keyStoreRef.get(addr)
|
|
1207
1222
|
if (apiKey !== null) {
|
package/src/accountConfig.ts
CHANGED
|
@@ -27,6 +27,17 @@ const ACCOUNT_TYPE_INT_TO_WIRE: Readonly<Record<number, string>> = {
|
|
|
27
27
|
[LIGHTER_ACCOUNT_TYPE_PREMIUM]: 'premium',
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// `account_trading_mode` on `DetailedAccount`: 0 = Classic/Simple (segregated
|
|
31
|
+
// margin), 1 = Unified Trading Account (cross-asset margin). Wire strings match
|
|
32
|
+
// the backend descriptor's ParamOption values. An unmapped int projects to null.
|
|
33
|
+
const LIGHTER_ACCOUNT_MODE_SIMPLE = 0
|
|
34
|
+
const LIGHTER_ACCOUNT_MODE_UNIFIED = 1
|
|
35
|
+
|
|
36
|
+
const ACCOUNT_MODE_INT_TO_WIRE: Readonly<Record<number, string>> = {
|
|
37
|
+
[LIGHTER_ACCOUNT_MODE_SIMPLE]: 'simpleTradingAccount',
|
|
38
|
+
[LIGHTER_ACCOUNT_MODE_UNIFIED]: 'unifiedTradingAccount',
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Project a single Lighter descriptor against the typed
|
|
32
43
|
* `LighterAccountConfig` into an `AccountConfigSetting`.
|
|
@@ -37,8 +48,9 @@ const ACCOUNT_TYPE_INT_TO_WIRE: Readonly<Record<number, string>> = {
|
|
|
37
48
|
* |---------------------------|--------------------------------------------------
|
|
38
49
|
* | REGISTER_API_KEY | [] (no parameters)
|
|
39
50
|
* | APPROVE_READ_ONLY_TOKEN | [] (no parameters)
|
|
40
|
-
* |
|
|
41
|
-
* |
|
|
51
|
+
* | SET_REFERRAL | [] (no parameters)
|
|
52
|
+
* | APPROVE_INTEGRATOR | [] (no parameters)
|
|
53
|
+
* | ACCOUNT_MODE | [{ name: 'mode', value: config.accountTradingMode }]
|
|
42
54
|
* | ACCOUNT_TYPE | [{ name: 'tier', value: config.accountType }]
|
|
43
55
|
*
|
|
44
56
|
* The switch is exhaustive over `ActionType` so enum additions force a
|
|
@@ -70,13 +82,25 @@ function projectLighterDescriptor(
|
|
|
70
82
|
satisfied: config.readOnlyTokenApproved,
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
85
|
+
// Backend-gated setup steps: `checkSetup` → `createAction` decides
|
|
86
|
+
// satisfaction (the backend emits no actions once satisfied), so the
|
|
87
|
+
// projection carries no local state.
|
|
88
|
+
case ActionType.SET_REFERRAL:
|
|
89
|
+
case ActionType.APPROVE_INTEGRATOR:
|
|
90
|
+
return { type: descriptor.type, values: [] }
|
|
91
|
+
|
|
92
|
+
// `mode` decodes the raw `account_trading_mode` integer to the descriptor's
|
|
93
|
+
// wire strings (`unifiedTradingAccount` / `simpleTradingAccount`).
|
|
94
|
+
// Unrecognised integers project to `null`.
|
|
76
95
|
case ActionType.ACCOUNT_MODE:
|
|
77
96
|
return {
|
|
78
97
|
type: descriptor.type,
|
|
79
|
-
values: [
|
|
98
|
+
values: [
|
|
99
|
+
{
|
|
100
|
+
name: 'mode',
|
|
101
|
+
value: ACCOUNT_MODE_INT_TO_WIRE[config.accountTradingMode] ?? null,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
// `tier` is the wire string `/changeAccountTier` accepts; we decode the
|
|
@@ -97,8 +121,6 @@ function projectLighterDescriptor(
|
|
|
97
121
|
|
|
98
122
|
case ActionType.APPROVE_AGENT:
|
|
99
123
|
case ActionType.APPROVE_BUILDER_FEE:
|
|
100
|
-
case ActionType.APPROVE_INTEGRATOR:
|
|
101
|
-
case ActionType.SET_REFERRAL:
|
|
102
124
|
case ActionType.SEND_ASSET:
|
|
103
125
|
case ActionType.WITHDRAWAL:
|
|
104
126
|
case ActionType.TRANSFER:
|
|
@@ -109,6 +131,7 @@ function projectLighterDescriptor(
|
|
|
109
131
|
case ActionType.MODIFY_ORDER:
|
|
110
132
|
case ActionType.UPDATE_LEVERAGE:
|
|
111
133
|
case ActionType.UPDATE_POSITION_MARGIN:
|
|
134
|
+
case ActionType.UPDATE_ASSET_COLLATERAL:
|
|
112
135
|
case ActionType.DEPOSIT:
|
|
113
136
|
case ActionType.META_VOTE:
|
|
114
137
|
case ActionType.META_ACCEPT_TERMS:
|
package/src/constants.ts
CHANGED
|
@@ -83,8 +83,18 @@ const LIGHTER_INVALID_AUTH_CODE = 20013
|
|
|
83
83
|
*/
|
|
84
84
|
const LIGHTER_SUCCESS_CODES = new Set([0, 200])
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Lighter body `code` meaning "operation succeeded" on the non-`/sendTx`
|
|
88
|
+
* mutation endpoints (`/changeAccountTier`, `/referral/use`). Any other code is
|
|
89
|
+
* a business-rule rejection surfaced verbatim to the caller.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
const LIGHTER_MUTATION_SUCCESS_CODE = 200
|
|
94
|
+
|
|
86
95
|
export {
|
|
87
96
|
LIGHTER_CODE_ACCOUNT_NOT_FOUND,
|
|
88
97
|
LIGHTER_INVALID_AUTH_CODE,
|
|
98
|
+
LIGHTER_MUTATION_SUCCESS_CODE,
|
|
89
99
|
LIGHTER_SUCCESS_CODES,
|
|
90
100
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ActionType } from '@lifi/perps-types'
|
|
2
2
|
import { DEFAULT_LIGHTER_REST_URL } from '../constants.js'
|
|
3
|
+
import { assetMarginModeInt } from '../utils/assetCollateral.js'
|
|
3
4
|
import {
|
|
4
5
|
type LighterWasmExports,
|
|
5
6
|
type LoadLighterWasmOptions,
|
|
@@ -425,6 +426,23 @@ export class LighterSigner {
|
|
|
425
426
|
ctx.apiKeyIndex,
|
|
426
427
|
ctx.accountIndex
|
|
427
428
|
)
|
|
429
|
+
case ActionType.ACCOUNT_MODE:
|
|
430
|
+
return wasm.SignUpdateAccountConfig(
|
|
431
|
+
numberField(p, 'account_trading_mode'),
|
|
432
|
+
SKIP_NONCE_DISABLED,
|
|
433
|
+
nonce,
|
|
434
|
+
ctx.apiKeyIndex,
|
|
435
|
+
ctx.accountIndex
|
|
436
|
+
)
|
|
437
|
+
case ActionType.UPDATE_ASSET_COLLATERAL:
|
|
438
|
+
return wasm.SignUpdateAccountAssetConfig(
|
|
439
|
+
numberField(p, 'asset_index'),
|
|
440
|
+
assetMarginModeInt(booleanField(p, 'enabled')),
|
|
441
|
+
SKIP_NONCE_DISABLED,
|
|
442
|
+
nonce,
|
|
443
|
+
ctx.apiKeyIndex,
|
|
444
|
+
ctx.accountIndex
|
|
445
|
+
)
|
|
428
446
|
default:
|
|
429
447
|
throw new Error(
|
|
430
448
|
`Lighter WASM signer does not support action: ${action}`
|
|
@@ -481,6 +499,16 @@ function optionalNumberField(
|
|
|
481
499
|
return numberField(p, key)
|
|
482
500
|
}
|
|
483
501
|
|
|
502
|
+
function booleanField(p: Record<string, unknown>, key: string): boolean {
|
|
503
|
+
const v = p[key]
|
|
504
|
+
if (typeof v === 'boolean') {
|
|
505
|
+
return v
|
|
506
|
+
}
|
|
507
|
+
throw new Error(
|
|
508
|
+
`Lighter sign params missing boolean field '${key}' (got ${typeof v})`
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
484
512
|
function stringField(p: Record<string, unknown>, key: string): string {
|
|
485
513
|
const v = p[key]
|
|
486
514
|
if (typeof v === 'string' && v !== '') {
|
|
@@ -11,20 +11,29 @@ import { ActionType, PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
|
|
|
11
11
|
import type { Address } from 'viem'
|
|
12
12
|
import { parseAbi } from 'viem'
|
|
13
13
|
import { waitForTransactionReceipt } from 'viem/actions'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_API_KEY_INDEX,
|
|
16
|
+
LIGHTER_MUTATION_SUCCESS_CODE,
|
|
17
|
+
} from '../constants.js'
|
|
18
|
+
import type { ApiParams, LighterApiClient } from '../utils/apiClient.js'
|
|
15
19
|
import type { LighterApiKey, LighterKeyStore } from './LighterKeyStore.js'
|
|
16
20
|
import type { LighterSigner } from './LighterSigner.js'
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Per-batch dependencies the Lighter `signActions` implementation needs:
|
|
20
24
|
* a WASM signer (`LighterSigner`), the API-key store keyed on L1 address,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* a REST client for the token-authenticated venue mutations that execute
|
|
26
|
+
* client-side, and a reference to the host SDK client for descriptor/account
|
|
27
|
+
* fetches within the REGISTER_API_KEY hybrid flow.
|
|
23
28
|
* @internal
|
|
24
29
|
*/
|
|
25
30
|
export interface LighterSignActionsDeps {
|
|
26
31
|
signer: LighterSigner
|
|
27
32
|
keyStore: LighterKeyStore
|
|
33
|
+
/** REST client bound to the user's Lighter base URL, used for the direct
|
|
34
|
+
* `changeAccountTier` / `referral/use` POSTs so their auth token never
|
|
35
|
+
* transits the LI.FI backend. */
|
|
36
|
+
apiClient: LighterApiClient
|
|
28
37
|
/**
|
|
29
38
|
* Resolve the user's Lighter `accountIndex` for an L1 address. Required
|
|
30
39
|
* by REGISTER_API_KEY so the ChangePubKey blob carries the right
|
|
@@ -34,15 +43,32 @@ export interface LighterSignActionsDeps {
|
|
|
34
43
|
resolveAccountIndex(address: Address): Promise<number>
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* `wasmSignParams.kind` values for the Lighter mutations that authenticate with
|
|
48
|
+
* a short-lived auth token instead of a wasm-signed transaction. Both execute
|
|
49
|
+
* client-side (SDK → Lighter REST) so the token never reaches the backend.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const TOKEN_AUTH_MUTATION_KINDS = new Set(['changeAccountTier', 'referralUse'])
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lifetime of the per-call auth token minted for a token-authenticated venue
|
|
56
|
+
* mutation. Minutes, not the read endpoints' hours: it authenticates one POST
|
|
57
|
+
* that completes immediately, so it never lingers usable.
|
|
58
|
+
*/
|
|
59
|
+
const TOKEN_AUTH_MUTATION_DEADLINE_SECONDS = 5 * 60
|
|
60
|
+
|
|
37
61
|
/**
|
|
38
62
|
* Sign a `WASM_BLOB` batch (Lighter). Ensures the user's API keypair is
|
|
39
63
|
* registered first — generating one and running the REGISTER_API_KEY
|
|
40
64
|
* hybrid flow via the L1 signer if not — then feeds each subsequent step
|
|
41
65
|
* through the WASM signer.
|
|
42
66
|
*
|
|
43
|
-
* `ACCOUNT_TYPE`
|
|
44
|
-
*
|
|
45
|
-
*
|
|
67
|
+
* The token-authenticated mutations (`ACCOUNT_TYPE` → `/changeAccountTier`,
|
|
68
|
+
* `SET_REFERRAL` → `/referral/use`) are NOT wasm-signed: they execute
|
|
69
|
+
* client-side here via a direct Lighter POST authenticated with a short-lived
|
|
70
|
+
* auth token, so the token never transits the LI.FI backend. They produce no
|
|
71
|
+
* backend-bound step and are omitted from the returned array.
|
|
46
72
|
* @internal
|
|
47
73
|
*/
|
|
48
74
|
export async function signWasmBlobActions(
|
|
@@ -55,8 +81,10 @@ export async function signWasmBlobActions(
|
|
|
55
81
|
for (const step of steps) {
|
|
56
82
|
if (step.action === ActionType.REGISTER_API_KEY) {
|
|
57
83
|
signed.push(await signRegisterApiKey(deps, address, step, ctx))
|
|
58
|
-
} else if (
|
|
59
|
-
|
|
84
|
+
} else if (
|
|
85
|
+
TOKEN_AUTH_MUTATION_KINDS.has(step.wasmSignParams.kind as string)
|
|
86
|
+
) {
|
|
87
|
+
await executeTokenAuthMutation(deps, address, step)
|
|
60
88
|
} else {
|
|
61
89
|
signed.push(await signStandardWasmAction(deps, address, step))
|
|
62
90
|
}
|
|
@@ -169,44 +197,94 @@ async function signRegisterApiKey(
|
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* Lighter's `/api/v1/changeAccountTier` is an HTTP-only mutation —
|
|
175
|
-
* Lighter does NOT consume a wasm-signed transaction here; it
|
|
176
|
-
* authenticates the request with the same auth token its read endpoints
|
|
177
|
-
* use, and enforces anti-replay business rules server-side. The backend
|
|
178
|
-
* declares the step as a `WasmBlobActionStep` with
|
|
179
|
-
* `wasmSignParams.kind = 'changeAccountTier'` and expects the SDK to create
|
|
180
|
-
* an auth token in lieu of a transaction signature; the executor reads
|
|
181
|
-
* `signedTx.txInfo` as the `auth` form parameter.
|
|
200
|
+
* Execute a token-authenticated Lighter mutation (`ACCOUNT_TYPE` →
|
|
201
|
+
* `/changeAccountTier`, `SET_REFERRAL` → `/referral/use`) client-side.
|
|
182
202
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
203
|
+
* Neither endpoint consumes a wasm-signed transaction — both authenticate with
|
|
204
|
+
* a Lighter auth token and enforce their business rules (open positions,
|
|
205
|
+
* pending orders, 24h tier cooldown) server-side. We mint a fresh short-lived
|
|
206
|
+
* token per call (never the stored read-only token, never persisted) and POST
|
|
207
|
+
* the mirror of the backend's form body directly to Lighter, so no auth token
|
|
208
|
+
* ever transits the LI.FI backend. A non-success verdict surfaces Lighter's
|
|
209
|
+
* `code`/`message` verbatim as an {@link PerpsErrorCode.ExchangeRejected}.
|
|
186
210
|
*/
|
|
187
|
-
async function
|
|
211
|
+
async function executeTokenAuthMutation(
|
|
188
212
|
deps: LighterSignActionsDeps,
|
|
189
213
|
address: Address,
|
|
190
214
|
step: WasmBlobActionStep
|
|
191
|
-
): Promise<
|
|
215
|
+
): Promise<void> {
|
|
192
216
|
const apiKey = await requireApiKey(deps, address)
|
|
193
|
-
const deadline =
|
|
217
|
+
const deadline =
|
|
218
|
+
Math.floor(Date.now() / 1000) + TOKEN_AUTH_MUTATION_DEADLINE_SECONDS
|
|
194
219
|
const authToken = await deps.signer.createAuthToken(deadline, {
|
|
195
220
|
apiKeyPrivateKey: apiKey.apiKeyPrivateKey,
|
|
196
221
|
apiKeyIndex: apiKey.apiKeyIndex,
|
|
197
222
|
accountIndex: apiKey.accountIndex,
|
|
198
223
|
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
224
|
+
|
|
225
|
+
const { path, params } = buildTokenAuthMutationRequest(step, authToken)
|
|
226
|
+
const { status, data } = await deps.apiClient.postForm<{
|
|
227
|
+
code?: number
|
|
228
|
+
message?: string
|
|
229
|
+
}>(path, params)
|
|
230
|
+
|
|
231
|
+
const code = data?.code
|
|
232
|
+
if (status < 200 || status >= 300 || code !== LIGHTER_MUTATION_SUCCESS_CODE) {
|
|
233
|
+
const suffix = data?.message ? `: ${data.message}` : ''
|
|
234
|
+
throw new PerpsError(
|
|
235
|
+
PerpsErrorCode.ExchangeRejected,
|
|
236
|
+
`Lighter ${step.action} rejected (code ${code ?? status})${suffix}`
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Map a token-authenticated mutation step to its Lighter endpoint and
|
|
243
|
+
* form body, mirroring the backend's request contract. The auth token is the
|
|
244
|
+
* `auth` field; the remaining fields come from `wasmSignParams`.
|
|
245
|
+
*/
|
|
246
|
+
function buildTokenAuthMutationRequest(
|
|
247
|
+
step: WasmBlobActionStep,
|
|
248
|
+
authToken: string
|
|
249
|
+
): { path: string; params: ApiParams } {
|
|
250
|
+
const kind = step.wasmSignParams.kind
|
|
251
|
+
if (kind === 'changeAccountTier') {
|
|
252
|
+
const { account_index, new_tier } = step.wasmSignParams as {
|
|
253
|
+
account_index?: number
|
|
254
|
+
new_tier?: string
|
|
255
|
+
}
|
|
256
|
+
if (account_index == null || new_tier == null) {
|
|
257
|
+
throw new PerpsError(
|
|
258
|
+
PerpsErrorCode.SDKError,
|
|
259
|
+
'Lighter ACCOUNT_TYPE wasmSignParams missing account_index or new_tier'
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
path: '/api/v1/changeAccountTier',
|
|
264
|
+
params: { auth: authToken, account_index, new_tier },
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (kind === 'referralUse') {
|
|
268
|
+
const { l1_address, referral_code, x } = step.wasmSignParams as {
|
|
269
|
+
l1_address?: string
|
|
270
|
+
referral_code?: string
|
|
271
|
+
x?: string
|
|
272
|
+
}
|
|
273
|
+
if (l1_address == null || referral_code == null || x == null) {
|
|
274
|
+
throw new PerpsError(
|
|
275
|
+
PerpsErrorCode.SDKError,
|
|
276
|
+
'Lighter SET_REFERRAL wasmSignParams missing l1_address, referral_code, or x'
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
path: '/api/v1/referral/use',
|
|
281
|
+
params: { auth: authToken, l1_address, referral_code, x },
|
|
282
|
+
}
|
|
209
283
|
}
|
|
284
|
+
throw new PerpsError(
|
|
285
|
+
PerpsErrorCode.SDKError,
|
|
286
|
+
`Lighter token-auth mutation: unknown wasmSignParams.kind '${String(kind)}'`
|
|
287
|
+
)
|
|
210
288
|
}
|
|
211
289
|
|
|
212
290
|
async function requireApiKey(
|
|
@@ -266,16 +344,6 @@ export async function signEvmTxActions(
|
|
|
266
344
|
)
|
|
267
345
|
}
|
|
268
346
|
|
|
269
|
-
if (walletSigner.chain?.id !== params.chainId) {
|
|
270
|
-
throw new PerpsError(
|
|
271
|
-
PerpsErrorCode.SDKError,
|
|
272
|
-
`EVM_TX leg '${step.action}' targets chain ${params.chainId}, but the ` +
|
|
273
|
-
`connected wallet is on chain ${walletSigner.chain?.id ?? 'unknown'}. ` +
|
|
274
|
-
`Switch the wallet to chain ${params.chainId} before signing — the ` +
|
|
275
|
-
"SDK broadcasts on the wallet's active chain and does not switch it."
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
347
|
const txHash = await walletSigner.writeContract({
|
|
280
348
|
address: params.to,
|
|
281
349
|
abi: parseAbi(params.abi),
|
|
@@ -166,6 +166,18 @@ export interface LighterWasmExports {
|
|
|
166
166
|
* fee requires a non-nil integrator index.
|
|
167
167
|
*/
|
|
168
168
|
SignApproveIntegrator: (...args: unknown[]) => SignResult
|
|
169
|
+
/**
|
|
170
|
+
* 5 args: accountTradingMode (0 = Classic/Simple, 1 = Unified), skipNonce,
|
|
171
|
+
* then the trailing three. `accountTradingMode` is validated to {0, 1} by
|
|
172
|
+
* lighter-go before signing.
|
|
173
|
+
*/
|
|
174
|
+
SignUpdateAccountConfig: (...args: unknown[]) => SignResult
|
|
175
|
+
/**
|
|
176
|
+
* 6 args: assetIndex, assetMarginMode (0 = MarginDisabled, 1 = MarginEnabled),
|
|
177
|
+
* skipNonce, then the trailing three. Keyed per spot asset, not per market —
|
|
178
|
+
* signs `L2UpdateAccountAssetConfigTx` (tx type 42).
|
|
179
|
+
*/
|
|
180
|
+
SignUpdateAccountAssetConfig: (...args: unknown[]) => SignResult
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
/** @internal */
|
|
@@ -191,6 +203,8 @@ const WASM_FUNCTION_NAMES = [
|
|
|
191
203
|
'SignModifyOrder',
|
|
192
204
|
'SignUpdateMargin',
|
|
193
205
|
'SignApproveIntegrator',
|
|
206
|
+
'SignUpdateAccountConfig',
|
|
207
|
+
'SignUpdateAccountAssetConfig',
|
|
194
208
|
] as const
|
|
195
209
|
|
|
196
210
|
type GoClass = new () => {
|
package/src/types/account.ts
CHANGED
|
@@ -29,6 +29,12 @@ export type LtAccountAsset = {
|
|
|
29
29
|
asset_id: number
|
|
30
30
|
balance: string
|
|
31
31
|
locked_balance: string
|
|
32
|
+
/**
|
|
33
|
+
* Cross-margin collateral flag: 0 = MarginDisabled, 1 = MarginEnabled.
|
|
34
|
+
* Lighter surfaces it via `additional_properties`, so it may be absent —
|
|
35
|
+
* chiefly outside Unified Trading Account mode.
|
|
36
|
+
*/
|
|
37
|
+
margin_mode?: number
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
package/src/utils/apiClient.ts
CHANGED
|
@@ -160,6 +160,31 @@ export class LighterApiClient {
|
|
|
160
160
|
return { status: response.status, data }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Form-encoded POST to a Lighter mutation endpoint. Single-shot — never
|
|
165
|
+
* retried, since these are money/state writes whose outcome is unknown on a
|
|
166
|
+
* transport failure. Surfaces the raw `{status, body}` pair so the caller can
|
|
167
|
+
* map Lighter's per-endpoint business-rule `code` to a domain error verbatim.
|
|
168
|
+
*/
|
|
169
|
+
async postForm<T>(
|
|
170
|
+
path: string,
|
|
171
|
+
params: ApiParams
|
|
172
|
+
): Promise<{ status: number; data: T }> {
|
|
173
|
+
const body = new URLSearchParams()
|
|
174
|
+
for (const [k, v] of Object.entries(params)) {
|
|
175
|
+
body.set(k, String(v))
|
|
176
|
+
}
|
|
177
|
+
const fetchImpl = this.fetchImpl ?? fetch
|
|
178
|
+
const response = await fetchImpl(`${this.baseUrl}${path}`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
181
|
+
body: body.toString(),
|
|
182
|
+
signal: this.signal,
|
|
183
|
+
})
|
|
184
|
+
const data = (await response.json().catch(() => undefined)) as T
|
|
185
|
+
return { status: response.status, data }
|
|
186
|
+
}
|
|
187
|
+
|
|
163
188
|
private async getChecked<T>(path: string, params?: ApiParams): Promise<T> {
|
|
164
189
|
const { status, data } = await this.getWithStatus<unknown>(path, params)
|
|
165
190
|
this.assertOk(path, status, data)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Lighter's `AssetMarginMode` wire ints (lighter-go
|
|
2
|
+
// `types/txtypes/update_account_asset_config.go`), used for both the write
|
|
3
|
+
// direction (signing `L2UpdateAccountAssetConfigTx`, tx type 42) and decoding
|
|
4
|
+
// an account asset's `margin_mode` on the read side.
|
|
5
|
+
const ASSET_MARGIN_DISABLED = 0
|
|
6
|
+
const ASSET_MARGIN_ENABLED = 1
|
|
7
|
+
|
|
8
|
+
/** Map the user-facing `enabled` flag to Lighter's `AssetMarginMode` wire int. */
|
|
9
|
+
export function assetMarginModeInt(enabled: boolean): number {
|
|
10
|
+
return enabled ? ASSET_MARGIN_ENABLED : ASSET_MARGIN_DISABLED
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Decode Lighter's `margin_mode` int on an account asset back to `enabled`. */
|
|
14
|
+
export function isAssetMarginEnabled(marginMode: number): boolean {
|
|
15
|
+
return marginMode === ASSET_MARGIN_ENABLED
|
|
16
|
+
}
|