@lifi/perps-sdk-provider-lighter 38.1.0 → 40.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.
Files changed (37) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +1 -0
  4. package/dist/cjs/LighterProvider.js.map +1 -1
  5. package/dist/cjs/copy.d.ts +5 -0
  6. package/dist/cjs/copy.d.ts.map +1 -0
  7. package/dist/cjs/copy.js +11 -0
  8. package/dist/cjs/copy.js.map +1 -0
  9. package/dist/cjs/signers/signActions.d.ts +6 -5
  10. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  11. package/dist/cjs/signers/signActions.js +180 -65
  12. package/dist/cjs/signers/signActions.js.map +1 -1
  13. package/dist/cjs/utils/transferMargin.js +2 -2
  14. package/dist/cjs/utils/transferMargin.js.map +1 -1
  15. package/dist/esm/LighterProvider.d.ts.map +1 -1
  16. package/dist/esm/LighterProvider.js +1 -0
  17. package/dist/esm/LighterProvider.js.map +1 -1
  18. package/dist/esm/copy.d.ts +5 -0
  19. package/dist/esm/copy.d.ts.map +1 -0
  20. package/dist/esm/copy.js +5 -0
  21. package/dist/esm/copy.js.map +1 -0
  22. package/dist/esm/signers/signActions.d.ts +18 -9
  23. package/dist/esm/signers/signActions.d.ts.map +1 -1
  24. package/dist/esm/signers/signActions.js +197 -89
  25. package/dist/esm/signers/signActions.js.map +1 -1
  26. package/dist/esm/utils/transferMargin.js +3 -3
  27. package/dist/esm/utils/transferMargin.js.map +1 -1
  28. package/dist/types/LighterProvider.d.ts.map +1 -1
  29. package/dist/types/copy.d.ts +5 -0
  30. package/dist/types/copy.d.ts.map +1 -0
  31. package/dist/types/signers/signActions.d.ts +18 -9
  32. package/dist/types/signers/signActions.d.ts.map +1 -1
  33. package/package.json +3 -3
  34. package/src/LighterProvider.ts +3 -2
  35. package/src/copy.ts +23 -0
  36. package/src/signers/signActions.ts +231 -107
  37. package/src/utils/transferMargin.ts +4 -4
@@ -1,20 +1,35 @@
1
- import { PerpsError, type SignActionsContext } from '@lifi/perps-sdk'
1
+ import {
2
+ PerpsError,
3
+ rethrowWithKnownResults,
4
+ type SignActionsContext,
5
+ type SignActionsResult,
6
+ } from '@lifi/perps-sdk'
2
7
  import type {
8
+ ActionResult,
3
9
  ActionStep,
4
10
  EvmTxActionStep,
5
11
  EvmTxSignedActionStep,
6
- SignedActionStep,
7
12
  WasmBlobActionStep,
8
13
  WasmBlobSignedActionStep,
9
14
  } from '@lifi/perps-types'
10
- import { ActionType, PerpsErrorCode, SigningMethod } from '@lifi/perps-types'
11
- import type { Address } from 'viem'
12
- import { parseAbi } from 'viem'
15
+ import {
16
+ ActionType,
17
+ PerpsErrorCode,
18
+ SigningMethod,
19
+ UserActionStatus,
20
+ } from '@lifi/perps-types'
21
+ import type { Address, Hex } from 'viem'
22
+ import { parseAbi, WaitForTransactionReceiptTimeoutError } from 'viem'
13
23
  import { waitForTransactionReceipt } from 'viem/actions'
14
24
  import {
15
25
  LIGHTER_MUTATION_SUCCESS_CODE,
16
26
  LIGHTER_REFERRAL_ALREADY_USED_CODE,
17
27
  } from '../constants.js'
28
+ import {
29
+ rejectedFailedErrorText,
30
+ revertedFailedErrorText,
31
+ unconfirmedIndeterminateErrorText,
32
+ } from '../copy.js'
18
33
  import type { ApiParams, LighterApiClient } from '../utils/apiClient.js'
19
34
  import { fetchAppliedReferralCode } from '../utils/appliedReferralCode.js'
20
35
  import { lighterErrorCodeFromBody } from '../utils/lighterErrorCode.js'
@@ -39,6 +54,8 @@ import type {
39
54
  * @internal
40
55
  */
41
56
  export interface LighterSignActionsDeps {
57
+ /** Provider key (`lighter` or `lighter-rh`) named in result texts. */
58
+ providerKey: string
42
59
  signer: LighterSigner
43
60
  keyStore: LighterKeyStore
44
61
  /** REST client bound to the user's Lighter base URL, used for the direct
@@ -168,7 +185,8 @@ export const createLighterApiKeyFreshness = (): LighterApiKeyFreshness => {
168
185
  * `SET_REFERRAL` → `/referral/use`) are NOT wasm-signed: they execute
169
186
  * client-side here via a direct Lighter POST authenticated with a short-lived
170
187
  * auth token, so the token never transits the LI.FI backend. They produce no
171
- * backend-bound step and are omitted from the returned array.
188
+ * backend-bound step: each one that sends reports its result in `executed`,
189
+ * and signing stops at the first one without an `Accepted` result.
172
190
  * @internal
173
191
  */
174
192
  export async function signWasmBlobActions(
@@ -176,7 +194,7 @@ export async function signWasmBlobActions(
176
194
  address: Address,
177
195
  steps: WasmBlobActionStep[],
178
196
  ctx: SignActionsContext | undefined
179
- ): Promise<WasmBlobSignedActionStep[]> {
197
+ ): Promise<SignActionsResult> {
180
198
  // REGISTER_API_KEY replaces the slot's key, so for such a batch the stored
181
199
  // key is expected to differ from the registered one.
182
200
  const replacesApiKey = steps.some(
@@ -194,22 +212,33 @@ export async function signWasmBlobActions(
194
212
  }
195
213
 
196
214
  const signed: WasmBlobSignedActionStep[] = []
197
- for (const step of steps) {
198
- if (step.action === ActionType.REGISTER_API_KEY) {
199
- signed.push(await signRegisterApiKey(deps, address, step, ctx))
200
- } else if (step.action === ActionType.APPROVE_INTEGRATOR) {
201
- signed.push(await signApproveIntegrator(deps, address, step, ctx))
202
- } else if (step.action === ActionType.TRANSFER) {
203
- signed.push(await signTransfer(deps, address, step, ctx))
204
- } else if (
205
- TOKEN_AUTH_MUTATION_KINDS.has(step.wasmSignParams.kind as string)
206
- ) {
207
- await executeTokenAuthMutation(deps, address, step)
208
- } else {
209
- signed.push(await signStandardWasmAction(deps, address, step))
215
+ const executed: ActionResult[] = []
216
+ try {
217
+ for (const step of steps) {
218
+ if (step.action === ActionType.REGISTER_API_KEY) {
219
+ signed.push(await signRegisterApiKey(deps, address, step, ctx))
220
+ } else if (step.action === ActionType.APPROVE_INTEGRATOR) {
221
+ signed.push(await signApproveIntegrator(deps, address, step, ctx))
222
+ } else if (step.action === ActionType.TRANSFER) {
223
+ signed.push(await signTransfer(deps, address, step, ctx))
224
+ } else if (
225
+ TOKEN_AUTH_MUTATION_KINDS.has(step.wasmSignParams.kind as string)
226
+ ) {
227
+ const result = await executeTokenAuthMutation(deps, address, step)
228
+ if (result !== undefined) {
229
+ executed.push(result)
230
+ if (result.status !== UserActionStatus.Accepted) {
231
+ break
232
+ }
233
+ }
234
+ } else {
235
+ signed.push(await signStandardWasmAction(deps, address, step))
236
+ }
210
237
  }
238
+ } catch (error) {
239
+ rethrowWithKnownResults(executed, error)
211
240
  }
212
- return signed
241
+ return { signed, executed }
213
242
  }
214
243
 
215
244
  async function signStandardWasmAction(
@@ -444,29 +473,15 @@ async function signTransfer(
444
473
  }
445
474
 
446
475
  /**
447
- * Execute a token-authenticated Lighter mutation (`ACCOUNT_TYPE` →
448
- * `/changeAccountTier`, `SET_REFERRAL` → `/referral/use`) client-side.
449
- *
450
- * Neither endpoint consumes a wasm-signed transaction — both authenticate with
451
- * a Lighter auth token and enforce their business rules (open positions,
452
- * pending orders, 24h tier cooldown) server-side. We create a fresh short-lived
453
- * token per call (never the stored read-only token, never persisted) and POST
454
- * the mirror of the backend's form body directly to Lighter, so no auth token
455
- * ever transits the LI.FI backend.
456
- *
457
- * Lighter applies a referral code permanently at sign-up, so any code on the
458
- * account settles `SET_REFERRAL`: the executor reads the applied code first and
459
- * skips the POST when the account carries one, and a
460
- * {@link LIGHTER_REFERRAL_ALREADY_USED_CODE} verdict from the POST settles the
461
- * step the same way. Every other non-success verdict surfaces Lighter's
462
- * `code`/`message` verbatim, under the `PerpsErrorCode` the body code names
463
- * ({@link lighterErrorCodeFromBody}) or {@link PerpsErrorCode.ExchangeRejected}.
476
+ * Execute a token-auth mutation without sending its auth token to the backend.
477
+ * An existing referral settles without a result. Server errors and malformed
478
+ * verdicts are indeterminate: the mutation may already have been applied.
464
479
  */
465
480
  async function executeTokenAuthMutation(
466
481
  deps: LighterSignActionsDeps,
467
482
  address: Address,
468
483
  step: WasmBlobActionStep
469
- ): Promise<void> {
484
+ ): Promise<ActionResult | undefined> {
470
485
  const apiKey = await requireApiKey(deps, address)
471
486
  const authToken = await deps.signer.createAuthToken({
472
487
  apiKeyPrivateKey: apiKey.apiKeyPrivateKey,
@@ -482,35 +497,81 @@ async function executeTokenAuthMutation(
482
497
  const venueCode = await readAppliedReferralCode(deps, address, authToken)
483
498
  if (venueCode.length > 0) {
484
499
  await deps.keyStore.markReferralApplied(address, venueCode)
485
- return
500
+ return undefined
486
501
  }
487
502
  }
488
503
  }
489
504
 
490
505
  const { path, params } = buildTokenAuthMutationRequest(step)
491
- const { status, data } = await deps.apiClient.postForm<{
492
- code?: number
493
- message?: string
494
- }>(path, authToken, params)
506
+ let answer: {
507
+ status: number
508
+ data: { code?: unknown; message?: string } | undefined
509
+ }
510
+ try {
511
+ answer = await deps.apiClient.postForm(path, authToken, params)
512
+ } catch (error) {
513
+ if (
514
+ error instanceof PerpsError &&
515
+ error.code === PerpsErrorCode.RateLimitExceeded
516
+ ) {
517
+ throw error
518
+ }
519
+ return {
520
+ action: step.action,
521
+ status: UserActionStatus.Indeterminate,
522
+ error: unconfirmedIndeterminateErrorText(step.action, deps.providerKey),
523
+ errorCode:
524
+ error instanceof PerpsError ? error.code : PerpsErrorCode.ServerError,
525
+ }
526
+ }
495
527
 
528
+ const { status, data } = answer
496
529
  const code = data?.code
530
+ if (status >= 500 || typeof code !== 'number' || !Number.isInteger(code)) {
531
+ return {
532
+ action: step.action,
533
+ status: UserActionStatus.Indeterminate,
534
+ error: unconfirmedIndeterminateErrorText(step.action, deps.providerKey),
535
+ errorCode: PerpsErrorCode.ServerError,
536
+ }
537
+ }
497
538
  const referralAlreadyApplied =
498
539
  step.wasmSignParams.kind === 'referralUse' &&
499
540
  code === LIGHTER_REFERRAL_ALREADY_USED_CODE
500
- if (
501
- !referralAlreadyApplied &&
502
- (status < 200 || status >= 300 || code !== LIGHTER_MUTATION_SUCCESS_CODE)
503
- ) {
504
- const suffix = data?.message ? `: ${data.message}` : ''
505
- throw new PerpsError(
506
- lighterErrorCodeFromBody(code) ?? PerpsErrorCode.ExchangeRejected,
507
- `Lighter ${step.action} rejected (code ${code ?? status})${suffix}`
508
- )
541
+ if (referralAlreadyApplied) {
542
+ if (appliedReferralCode !== undefined) {
543
+ await deps.keyStore.markReferralApplied(address, appliedReferralCode)
544
+ }
545
+ return undefined
546
+ }
547
+ if (status < 200 || status >= 300 || code !== LIGHTER_MUTATION_SUCCESS_CODE) {
548
+ return {
549
+ action: step.action,
550
+ status: UserActionStatus.Failed,
551
+ error: rejectedFailedErrorText(
552
+ step.action,
553
+ deps.providerKey,
554
+ code,
555
+ status,
556
+ data?.message
557
+ ),
558
+ errorCode:
559
+ lighterErrorCodeFromBody(code) ?? PerpsErrorCode.ExchangeRejected,
560
+ }
509
561
  }
510
562
 
563
+ const accepted: ActionResult = {
564
+ action: step.action,
565
+ status: UserActionStatus.Accepted,
566
+ }
511
567
  if (appliedReferralCode !== undefined) {
512
- await deps.keyStore.markReferralApplied(address, appliedReferralCode)
568
+ try {
569
+ await deps.keyStore.markReferralApplied(address, appliedReferralCode)
570
+ } catch (error) {
571
+ rethrowWithKnownResults([accepted], error)
572
+ }
513
573
  }
574
+ return accepted
514
575
  }
515
576
 
516
577
  /**
@@ -603,9 +664,15 @@ async function requireApiKey(
603
664
  * Sign and broadcast a sequence of `EVM_TX` actions via the user's wallet
604
665
  * client. Each leg is broadcast then confirmed to a successful receipt before
605
666
  * the next is broadcast, so a later step (e.g. deposit) can rely on an earlier
606
- * step (e.g. approve) having mined. A reverted leg throws and aborts the
607
- * remaining legs. Each step's `txParams` carries chainId, target, function
608
- * name, args, and a human-readable abi from the backend.
667
+ * step (e.g. approve) having mined. Each step's `txParams` carries chainId,
668
+ * target, function name, args, and a human-readable abi from the backend.
669
+ *
670
+ * When every leg confirms, the legs are `signed` for `/executeAction`. A leg
671
+ * that reverts, or whose receipt does not arrive, aborts the remaining legs:
672
+ * the batch then returns no `signed` step and reports each confirmed leg as
673
+ * `Accepted`, then the reverted leg as a `Failed` result or the
674
+ * unconfirmed leg as `Indeterminate`. A leg that throws before its broadcast
675
+ * throws a `PerpsExecutionError` when an earlier leg confirmed.
609
676
  *
610
677
  * Before broadcasting a leg the SDK switches the wallet to the leg's
611
678
  * `txParams.chainId` via `ctx.switchToChain`, re-reading the returned client so
@@ -617,9 +684,10 @@ async function requireApiKey(
617
684
  * @internal
618
685
  */
619
686
  export async function signEvmTxActions(
687
+ providerKey: string,
620
688
  steps: EvmTxActionStep[],
621
689
  ctx: SignActionsContext | undefined
622
- ): Promise<EvmTxSignedActionStep[]> {
690
+ ): Promise<SignActionsResult> {
623
691
  const walletSigner = ctx?.userWallet
624
692
  if (!walletSigner) {
625
693
  throw new PerpsError(
@@ -632,70 +700,126 @@ export async function signEvmTxActions(
632
700
  const switchToChain = ctx?.switchToChain
633
701
  const onProgress = ctx?.onProgress
634
702
  const signed: EvmTxSignedActionStep[] = []
703
+ const confirmed = (): ActionResult[] =>
704
+ signed.map((leg) => ({
705
+ action: leg.action,
706
+ status: UserActionStatus.Accepted,
707
+ txHash: leg.txHash,
708
+ }))
635
709
  let legSigner = walletSigner
636
710
  for (const [index, step] of steps.entries()) {
637
711
  const params = step.txParams
638
712
 
639
- if (legSigner.chain?.id !== params.chainId && switchToChain) {
640
- legSigner = await switchToChain(params.chainId)
713
+ let txHash: Hex
714
+ try {
715
+ if (legSigner.chain?.id !== params.chainId && switchToChain) {
716
+ legSigner = await switchToChain(params.chainId)
717
+ }
718
+ if (legSigner.chain?.id !== params.chainId) {
719
+ throw new PerpsError(
720
+ PerpsErrorCode.SDKError,
721
+ `EVM_TX leg '${step.action}' targets chain ${params.chainId}, but the ` +
722
+ `connected wallet is on chain ${legSigner.chain?.id ?? 'unknown'} and ` +
723
+ 'no chain switch was performed. Configure a switchChain hook via ' +
724
+ `setSwitchChain, or connect the wallet to chain ${params.chainId} ` +
725
+ 'before signing.'
726
+ )
727
+ }
728
+
729
+ txHash = await legSigner.writeContract({
730
+ address: params.to,
731
+ abi: parseAbi(params.abi),
732
+ functionName: params.functionName,
733
+ args: params.args,
734
+ chain: legSigner.chain,
735
+ account: legSigner.account,
736
+ })
737
+ } catch (error) {
738
+ rethrowWithKnownResults(confirmed(), error)
641
739
  }
642
- if (legSigner.chain?.id !== params.chainId) {
643
- throw new PerpsError(
644
- PerpsErrorCode.SDKError,
645
- `EVM_TX leg '${step.action}' targets chain ${params.chainId}, but the ` +
646
- `connected wallet is on chain ${legSigner.chain?.id ?? 'unknown'} and ` +
647
- 'no chain switch was performed. Configure a switchChain hook via ' +
648
- `setSwitchChain, or connect the wallet to chain ${params.chainId} ` +
649
- 'before signing.'
740
+ try {
741
+ onProgress?.({
742
+ index,
743
+ total: steps.length,
744
+ action: step.action,
745
+ functionName: params.functionName,
746
+ chainId: params.chainId,
747
+ status: 'submitted',
748
+ txHash,
749
+ })
750
+ } catch (error) {
751
+ rethrowWithKnownResults(
752
+ [
753
+ ...confirmed(),
754
+ {
755
+ action: step.action,
756
+ status: UserActionStatus.Submitted,
757
+ txHash,
758
+ },
759
+ ],
760
+ error
650
761
  )
651
762
  }
652
763
 
653
- const txHash = await legSigner.writeContract({
654
- address: params.to,
655
- abi: parseAbi(params.abi),
656
- functionName: params.functionName,
657
- args: params.args,
658
- chain: legSigner.chain,
659
- account: legSigner.account,
660
- })
661
- onProgress?.({
662
- index,
663
- total: steps.length,
664
- action: step.action,
665
- functionName: params.functionName,
666
- chainId: params.chainId,
667
- status: 'submitted',
668
- txHash,
669
- })
670
-
671
- const receipt = await waitForTransactionReceipt(legSigner, {
672
- hash: txHash,
673
- })
764
+ let receipt: Awaited<ReturnType<typeof waitForTransactionReceipt>>
765
+ try {
766
+ receipt = await waitForTransactionReceipt(legSigner, {
767
+ hash: txHash,
768
+ })
769
+ } catch (error) {
770
+ return {
771
+ signed: [],
772
+ executed: [
773
+ ...confirmed(),
774
+ {
775
+ action: step.action,
776
+ status: UserActionStatus.Indeterminate,
777
+ txHash,
778
+ error: unconfirmedIndeterminateErrorText(step.action, providerKey),
779
+ errorCode:
780
+ error instanceof WaitForTransactionReceiptTimeoutError
781
+ ? PerpsErrorCode.TimeoutError
782
+ : PerpsErrorCode.ServerError,
783
+ },
784
+ ],
785
+ }
786
+ }
674
787
  if (receipt.status === 'reverted') {
675
- throw new PerpsError(
676
- PerpsErrorCode.SDKError,
677
- `EVM_TX leg '${step.action}' reverted (tx ${txHash}); aborting the ` +
678
- 'remaining legs.'
679
- )
788
+ return {
789
+ signed: [],
790
+ executed: [
791
+ ...confirmed(),
792
+ {
793
+ action: step.action,
794
+ status: UserActionStatus.Failed,
795
+ txHash,
796
+ error: revertedFailedErrorText(step.action, txHash),
797
+ errorCode: PerpsErrorCode.SDKError,
798
+ },
799
+ ],
800
+ }
680
801
  }
681
- onProgress?.({
682
- index,
683
- total: steps.length,
684
- action: step.action,
685
- functionName: params.functionName,
686
- chainId: params.chainId,
687
- status: 'confirmed',
688
- txHash,
689
- })
690
-
691
802
  signed.push({
692
803
  action: step.action,
693
804
  txParams: step.txParams,
694
805
  txHash,
695
806
  })
807
+ try {
808
+ onProgress?.({
809
+ index,
810
+ total: steps.length,
811
+ action: step.action,
812
+ functionName: params.functionName,
813
+ chainId: params.chainId,
814
+ status: 'confirmed',
815
+ txHash,
816
+ })
817
+ } catch (error) {
818
+ rethrowWithKnownResults(confirmed(), error)
819
+ }
696
820
  }
697
821
 
698
- return signed
822
+ return { signed, executed: [] }
699
823
  }
700
824
 
701
825
  /**
@@ -711,7 +835,7 @@ export async function lighterSignActions(
711
835
  steps: ActionStep[],
712
836
  address: Address,
713
837
  ctx?: SignActionsContext
714
- ): Promise<SignedActionStep[]> {
838
+ ): Promise<SignActionsResult> {
715
839
  switch (method) {
716
840
  case SigningMethod.WASM_BLOB:
717
841
  return signWasmBlobActions(
@@ -721,7 +845,7 @@ export async function lighterSignActions(
721
845
  ctx
722
846
  )
723
847
  case SigningMethod.EVM_TX:
724
- return signEvmTxActions(steps as EvmTxActionStep[], ctx)
848
+ return signEvmTxActions(deps.providerKey, steps as EvmTxActionStep[], ctx)
725
849
  case SigningMethod.EIP712:
726
850
  throw new PerpsError(
727
851
  PerpsErrorCode.SDKError,
@@ -2,9 +2,9 @@ import {
2
2
  addDecimalString,
3
3
  isDecimalString,
4
4
  isDecimalStringGreaterThan,
5
+ isPositionMarginAdjustable,
6
+ isPositionMarginRemovable,
5
7
  PerpsError,
6
- positionSupportsMarginAdjustment,
7
- positionSupportsMarginRemoval,
8
8
  roundDecimalString,
9
9
  subtractDecimalString,
10
10
  } from '@lifi/perps-sdk'
@@ -60,10 +60,10 @@ function positivePositionAmount(
60
60
  export function positionRemovableMargin(
61
61
  position: Position
62
62
  ): DecimalString | undefined {
63
- if (!positionSupportsMarginAdjustment(position)) {
63
+ if (!isPositionMarginAdjustable(position)) {
64
64
  return undefined
65
65
  }
66
- if (!positionSupportsMarginRemoval(position)) {
66
+ if (!isPositionMarginRemovable(position)) {
67
67
  return '0'
68
68
  }
69
69
  const removable = subtractDecimalString(