@lifi/perps-sdk 18.1.0 → 19.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 (59) hide show
  1. package/dist/cjs/client/PerpsClient.d.ts +13 -7
  2. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  3. package/dist/cjs/client/PerpsClient.js +141 -52
  4. package/dist/cjs/client/PerpsClient.js.map +1 -1
  5. package/dist/cjs/index.d.ts +1 -1
  6. package/dist/cjs/index.d.ts.map +1 -1
  7. package/dist/cjs/index.js +2 -1
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/types/api.d.ts +4 -2
  10. package/dist/cjs/types/api.d.ts.map +1 -1
  11. package/dist/cjs/types/provider.d.ts +4 -5
  12. package/dist/cjs/types/provider.d.ts.map +1 -1
  13. package/dist/cjs/utils/setupActions.d.ts +4 -2
  14. package/dist/cjs/utils/setupActions.d.ts.map +1 -1
  15. package/dist/cjs/utils/setupActions.js +9 -1
  16. package/dist/cjs/utils/setupActions.js.map +1 -1
  17. package/dist/cjs/utils/switchChain.js +1 -1
  18. package/dist/cjs/utils/switchChain.js.map +1 -1
  19. package/dist/cjs/version.d.ts +1 -1
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/esm/client/PerpsClient.d.ts +62 -54
  22. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  23. package/dist/esm/client/PerpsClient.js +204 -100
  24. package/dist/esm/client/PerpsClient.js.map +1 -1
  25. package/dist/esm/index.d.ts +1 -1
  26. package/dist/esm/index.d.ts.map +1 -1
  27. package/dist/esm/index.js +1 -1
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/types/api.d.ts +31 -14
  30. package/dist/esm/types/api.d.ts.map +1 -1
  31. package/dist/esm/types/provider.d.ts +21 -27
  32. package/dist/esm/types/provider.d.ts.map +1 -1
  33. package/dist/esm/utils/setupActions.d.ts +17 -8
  34. package/dist/esm/utils/setupActions.d.ts.map +1 -1
  35. package/dist/esm/utils/setupActions.js +20 -7
  36. package/dist/esm/utils/setupActions.js.map +1 -1
  37. package/dist/esm/utils/switchChain.js +1 -1
  38. package/dist/esm/utils/switchChain.js.map +1 -1
  39. package/dist/esm/version.d.ts +1 -1
  40. package/dist/esm/version.js +1 -1
  41. package/dist/types/client/PerpsClient.d.ts +62 -54
  42. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/types/api.d.ts +31 -14
  46. package/dist/types/types/api.d.ts.map +1 -1
  47. package/dist/types/types/provider.d.ts +21 -27
  48. package/dist/types/types/provider.d.ts.map +1 -1
  49. package/dist/types/utils/setupActions.d.ts +17 -8
  50. package/dist/types/utils/setupActions.d.ts.map +1 -1
  51. package/dist/types/version.d.ts +1 -1
  52. package/package.json +2 -2
  53. package/src/client/PerpsClient.ts +284 -129
  54. package/src/index.ts +4 -1
  55. package/src/types/api.ts +32 -14
  56. package/src/types/provider.ts +21 -29
  57. package/src/utils/setupActions.ts +27 -13
  58. package/src/utils/switchChain.ts +1 -1
  59. package/src/version.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AccountConfigSetting,
2
3
  AccountResponse,
3
4
  AccountSummary,
4
5
  ActionParamsMap,
@@ -16,9 +17,12 @@ import type {
16
17
  Position,
17
18
  Provider,
18
19
  ProviderAction,
20
+ SetupAction,
21
+ SetupOption,
19
22
  SignedActionStep,
20
23
  } from '@lifi/perps-types'
21
24
  import {
25
+ ActionRelay,
22
26
  ActionType,
23
27
  META_PROVIDER,
24
28
  PerpsErrorCode,
@@ -63,6 +67,7 @@ import type {
63
67
  PlaceTwapOrderParams,
64
68
  ProviderSetup,
65
69
  SendAssetActionParams,
70
+ SetupChecklistItem,
66
71
  SubmitOnboardingParams,
67
72
  WithdrawParams,
68
73
  } from '../types/api.js'
@@ -80,6 +85,7 @@ import type {
80
85
  SignActionsContext,
81
86
  } from '../types/provider.js'
82
87
  import type { WithdrawableBalance, WithdrawFlow } from '../types/withdrawal.js'
88
+ import { isUserFacingSetupStep } from '../utils/setupActions.js'
83
89
  import { signTypedDataWithSigner } from '../utils/signTypedData.js'
84
90
  import {
85
91
  eip712DomainChainId,
@@ -89,6 +95,127 @@ import {
89
95
  import { createPerpsClient } from './createPerpsClient.js'
90
96
  import { requireProvider as resolveProvider } from './requireProvider.js'
91
97
 
98
+ /** Absent `sequence` sorts last, so an unordered step gates nothing. */
99
+ function sequenceOf(descriptor: ProviderAction): number {
100
+ return descriptor.sequence ?? Number.MAX_SAFE_INTEGER
101
+ }
102
+
103
+ const SIGNERS: ReadonlySet<string> = new Set(Object.values(PerpsSigner))
104
+ const RELAYS: ReadonlySet<string> = new Set(Object.values(ActionRelay))
105
+
106
+ /**
107
+ * Reject provider metadata whose steps the SDK cannot classify. A step with no
108
+ * known `signer`, `relay`, `options` or `revoke` would otherwise read as
109
+ * satisfied and report a not-onboarded account as ready.
110
+ */
111
+ function assertSetupContract(metadata: Provider): void {
112
+ const reject = (step: ActionType, problem: string): never => {
113
+ throw new PerpsError(
114
+ PerpsErrorCode.SDKError,
115
+ `Provider '${metadata.key}' step '${step}' ${problem}; this SDK needs a ` +
116
+ `backend that emits signer, relay, options and revoke.`
117
+ )
118
+ }
119
+ for (const descriptor of [...metadata.setup, ...metadata.actions]) {
120
+ if (!SIGNERS.has(descriptor.signer)) {
121
+ reject(descriptor.type, 'declares no known signer')
122
+ }
123
+ if (!RELAYS.has(descriptor.relay)) {
124
+ reject(descriptor.type, 'declares no known relay')
125
+ }
126
+ }
127
+ const actionTypes = new Set(metadata.actions.map((d) => d.type))
128
+ for (const descriptor of metadata.setup) {
129
+ if (descriptor.options === undefined) {
130
+ reject(descriptor.type, 'declares no options')
131
+ }
132
+ if (descriptor.options !== null && descriptor.options.length === 0) {
133
+ reject(descriptor.type, 'declares an empty options list')
134
+ }
135
+ if (descriptor.revoke === undefined) {
136
+ reject(descriptor.type, 'declares no revoke')
137
+ }
138
+ if (descriptor.revoke !== null && !actionTypes.has(descriptor.revoke)) {
139
+ reject(
140
+ descriptor.type,
141
+ `revokes with '${descriptor.revoke}', which Provider.actions does not declare`
142
+ )
143
+ }
144
+ const defaults = (descriptor.options ?? []).filter(
145
+ (option) => option.default === true
146
+ )
147
+ if (defaults.length > 1) {
148
+ reject(descriptor.type, 'marks more than one option default')
149
+ }
150
+ }
151
+ }
152
+
153
+ /** Only a USER-signed non-choice step is staged for the user to sign. */
154
+ function isStageable(descriptor: SetupAction): boolean {
155
+ return descriptor.options === null && descriptor.signer === PerpsSigner.USER
156
+ }
157
+
158
+ function satisfiedTypes(settings: AccountConfigSetting[]): Set<ActionType> {
159
+ return new Set(
160
+ settings.filter((setting) => setting.satisfied).map((s) => s.type)
161
+ )
162
+ }
163
+
164
+ /** The choice option whose every bound param equals the projected value. */
165
+ function selectedOption(
166
+ descriptor: SetupAction,
167
+ settings: AccountConfigSetting[]
168
+ ): SetupOption | null {
169
+ if (descriptor.options === null) {
170
+ return null
171
+ }
172
+ const values =
173
+ settings.find((setting) => setting.type === descriptor.type)?.values ?? []
174
+ return (
175
+ descriptor.options.find((option) => {
176
+ const bound = Object.entries(option.params)
177
+ return (
178
+ bound.length > 0 &&
179
+ bound.every(([name, value]) =>
180
+ values.some((v) => v.name === name && v.value === value)
181
+ )
182
+ )
183
+ }) ?? null
184
+ )
185
+ }
186
+
187
+ /**
188
+ * The projected value of the param a choice's options bind, as the venue
189
+ * names it, whether or not an option binds that value.
190
+ */
191
+ function currentChoiceValue(
192
+ descriptor: SetupAction,
193
+ settings: AccountConfigSetting[]
194
+ ): string | null {
195
+ if (descriptor.options === null) {
196
+ return null
197
+ }
198
+ const names = new Set(
199
+ descriptor.options.flatMap((option) => Object.keys(option.params))
200
+ )
201
+ const value =
202
+ settings
203
+ .find((setting) => setting.type === descriptor.type)
204
+ ?.values.find((v) => names.has(v.name))?.value ?? null
205
+ return value === null ? null : String(value)
206
+ }
207
+
208
+ /** Throw the first per-action failure a 200 OK `/executeAction` carried. */
209
+ function assertAllSucceeded(results: ActionResult[]): void {
210
+ const failure = results.find((r) => !r.success)
211
+ if (failure) {
212
+ throw new PerpsError(
213
+ failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
214
+ failure.error
215
+ )
216
+ }
217
+ }
218
+
92
219
  function isNonNegativeDecimal(value: string): boolean {
93
220
  try {
94
221
  return new Big(value).gte(0)
@@ -105,11 +232,9 @@ function findActionDescriptor(
105
232
  metadata: Provider,
106
233
  action: ActionType
107
234
  ): ProviderAction {
108
- const descriptor = [
109
- ...metadata.setup,
110
- ...metadata.options,
111
- ...metadata.actions,
112
- ].find((d) => d.type === action)
235
+ const descriptor = [...metadata.setup, ...metadata.actions].find(
236
+ (d) => d.type === action
237
+ )
113
238
  if (!descriptor) {
114
239
  throw new PerpsError(
115
240
  PerpsErrorCode.SDKError,
@@ -171,7 +296,7 @@ export class PerpsClient {
171
296
 
172
297
  /**
173
298
  * Set or update the end-user's wallet. Used whenever an action's descriptor
174
- * names the user wallet in its `signers` list. Pass undefined to clear.
299
+ * names `PerpsSigner.USER` as its `signer`. Pass undefined to clear.
175
300
  *
176
301
  * @public
177
302
  */
@@ -205,17 +330,15 @@ export class PerpsClient {
205
330
  }
206
331
 
207
332
  private async getProviderMetadata(provider: string): Promise<Provider> {
208
- const cached = this.providerMetadataCache.get(provider)
209
- if (cached) {
210
- return cached
211
- }
212
-
213
- const { providers } = await getProviders(this.sdkClient)
214
- for (const d of providers) {
215
- this.providerMetadataCache.set(d.key, d)
333
+ let metadata = this.providerMetadataCache.get(provider)
334
+ if (!metadata) {
335
+ const { providers } = await getProviders(this.sdkClient)
336
+ for (const d of providers) {
337
+ this.providerMetadataCache.set(d.key, d)
338
+ }
339
+ metadata = this.providerMetadataCache.get(provider)
216
340
  }
217
341
 
218
- const metadata = this.providerMetadataCache.get(provider)
219
342
  if (!metadata) {
220
343
  const error = new PerpsError(
221
344
  PerpsErrorCode.SDKError,
@@ -224,6 +347,7 @@ export class PerpsClient {
224
347
  error.tool = '@lifi/perps-sdk'
225
348
  throw error
226
349
  }
350
+ assertSetupContract(metadata)
227
351
  return metadata
228
352
  }
229
353
 
@@ -256,7 +380,7 @@ export class PerpsClient {
256
380
  * Ask the provider plugin for the signer-bearing wire fields of `action` —
257
381
  * the on-wire `signerAddress` and any signer-derived params (e.g.
258
382
  * Hyperliquid's `agentAddress` for `APPROVE_AGENT`). Forwards the descriptor's
259
- * `signers` so the plugin can branch on signer role. Returns empty when the
383
+ * `signer` so the plugin can branch on signer role. Returns empty when the
260
384
  * plugin signs as the user or with a non-EVM credential. Core constructs no
261
385
  * `signerAddress` itself; signer identity is plugin-owned.
262
386
  */
@@ -272,17 +396,17 @@ export class PerpsClient {
272
396
  return plugin.resolveActionRequest(
273
397
  descriptor.type,
274
398
  address,
275
- descriptor.signers
399
+ descriptor.signer
276
400
  )
277
401
  }
278
402
 
279
403
  /**
280
404
  * Delegate signing of `actions` to the provider plugin, one call per
281
405
  * consecutive run of steps sharing a descriptor. Each call carries that
282
- * descriptor's `signingMethod` and `signers`, so a mixed batch signs every
406
+ * descriptor's `signingMethod` and `signer`, so a mixed batch signs every
283
407
  * step with the signer its own descriptor declares; the signed steps come
284
408
  * back in the batch's original order. The plugin owns every signing arm and
285
- * branches on `signers` internally, reading the end-user's wallet from the
409
+ * branches on `signer` internally, reading the end-user's wallet from the
286
410
  * {@link SignActionsContext} when an arm signs as the user.
287
411
  *
288
412
  * @throws {PerpsError} When a step's action is not declared by the provider,
@@ -405,9 +529,9 @@ export class PerpsClient {
405
529
 
406
530
  /**
407
531
  * Assemble the per-call context the provider plugin needs in order to sign:
408
- * the end-user's wallet and the descriptor's declared `signers`. Core
409
- * forwards `signers` as data so the plugin can pick WHO signs; it does not
410
- * branch on them. Provider-owned session credentials (the Hyperliquid agent
532
+ * the end-user's wallet and the descriptor's declared `signer`. Core
533
+ * forwards `signer` as data so the plugin can pick WHO signs; it does not
534
+ * branch on it. Provider-owned session credentials (the Hyperliquid agent
411
535
  * keypair, Lighter's API key) are resolved inside the provider's
412
536
  * `signActions`, not threaded through here.
413
537
  *
@@ -425,7 +549,7 @@ export class PerpsClient {
425
549
  userWallet?: PerpsClientSigner,
426
550
  onProgress?: (progress: SignActionProgress) => void
427
551
  ): SignActionsContext {
428
- const ctx: SignActionsContext = { signers: descriptor.signers }
552
+ const ctx: SignActionsContext = { signer: descriptor.signer }
429
553
  if (onProgress !== undefined) {
430
554
  ctx.onProgress = onProgress
431
555
  }
@@ -501,7 +625,7 @@ export class PerpsClient {
501
625
  /**
502
626
  * Fetch the user's account state from the backend and attach the
503
627
  * SDK-projected `settings` array — one `AccountConfigSetting` per
504
- * descriptor on `Provider.setup` + `Provider.options`. Callers read
628
+ * descriptor on `Provider.setup`. Callers read
505
629
  * `result.settings` directly without re-deriving values from the typed
506
630
  * `AccountConfig`.
507
631
  *
@@ -518,11 +642,7 @@ export class PerpsClient {
518
642
  fetchAccount(this.sdkClient, params),
519
643
  this.getProviderMetadata(params.provider),
520
644
  ])
521
- const settings = plugin.projectConfig(
522
- response.config,
523
- metadata.setup,
524
- metadata.options
525
- )
645
+ const settings = plugin.projectConfig(response.config, metadata.setup)
526
646
  return { ...response, settings }
527
647
  }
528
648
 
@@ -773,15 +893,17 @@ export class PerpsClient {
773
893
  }
774
894
 
775
895
  /**
776
- * Return the unsatisfied entries on `Provider.setup` for this account as a
777
- * flat, self-describing list. Trading is gated on `isReady === true`.
778
- * `checklist` carries the renderable onboarding list: every USER-signed
779
- * descriptor with its satisfied state, with not-required conditional steps
780
- * omitted.
896
+ * Return the setup steps this account must still sign as a flat,
897
+ * self-describing list. Trading is gated on `isReady === true`.
898
+ * `checklist` carries the renderable onboarding list: every choice step and
899
+ * every `USER`-signed step with its satisfied state, with not-required
900
+ * conditional steps omitted.
781
901
  *
782
- * `Provider.options` descriptors are NEVER returned here — options are
783
- * post-setup tunables and never gate trading. Option state is surfaced
784
- * separately via `getAccount().settings`.
902
+ * A non-choice `SDK`-signed step is NEVER returned here — the SDK drains it
903
+ * with the provider's own credentials. A choice is listed but never staged
904
+ * or executed, whatever its `signer`: the user picks an option through
905
+ * {@link executeProviderOption}, and `isReady` stays `false` until every
906
+ * choice on `checklist` is satisfied.
785
907
  *
786
908
  * @public
787
909
  */
@@ -807,19 +929,15 @@ export class PerpsClient {
807
929
  }
808
930
  }
809
931
 
810
- const satisfiedSetup = await this.resolveSatisfiedSetup(provider, address)
932
+ const settings = await this.resolveSetupSettings(provider, address)
933
+ const satisfiedSetup = satisfiedTypes(settings)
811
934
  const pendingSetup = metadata.setup.filter(
812
935
  (descriptor) => !satisfiedSetup.has(descriptor.type)
813
936
  )
814
937
 
815
938
  const plugin = this.sdkClient.getProvider(provider)
816
- const internalTypes = new Set(plugin?.internalSetupActions ?? [])
817
- const isInternal = (descriptor: ProviderAction): boolean =>
818
- internalTypes.has(descriptor.type) &&
819
- !descriptor.signers.includes(PerpsSigner.USER)
820
- const visibleSetup = pendingSetup.filter(
821
- (descriptor) => !isInternal(descriptor)
822
- )
939
+
940
+ const stageable = pendingSetup.filter(isStageable)
823
941
 
824
942
  // The backend filters already-satisfied setup actions and returns typed
825
943
  // data for those still outstanding; each plugin contributes its own
@@ -827,7 +945,7 @@ export class PerpsClient {
827
945
  const actions = await this.buildProviderSetupActions(
828
946
  provider,
829
947
  address,
830
- visibleSetup
948
+ stageable
831
949
  )
832
950
 
833
951
  // A staged step is one the build produced actions for — a pending
@@ -835,69 +953,64 @@ export class PerpsClient {
835
953
  // (backend-gated) or not applicable to this account.
836
954
  const stagedTypes = new Set(actions.map((step) => step.action))
837
955
  const conditionalTypes = new Set(plugin?.conditionalSetupActions ?? [])
838
- const sequenceOf = (descriptor: ProviderAction): number =>
839
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
840
956
  const checklist = metadata.setup
841
- .filter((descriptor) => descriptor.signers.includes(PerpsSigner.USER))
957
+ .filter(isUserFacingSetupStep)
842
958
  .filter(
843
959
  (descriptor) =>
960
+ descriptor.options !== null ||
844
961
  !conditionalTypes.has(descriptor.type) ||
845
962
  stagedTypes.has(descriptor.type)
846
963
  )
847
964
  .sort((a, b) => sequenceOf(a) - sequenceOf(b))
848
965
  .map((descriptor) => ({
849
966
  descriptor,
850
- satisfied: !stagedTypes.has(descriptor.type),
967
+ satisfied:
968
+ descriptor.options !== null
969
+ ? satisfiedSetup.has(descriptor.type)
970
+ : !stagedTypes.has(descriptor.type),
971
+ selected: selectedOption(descriptor, settings),
972
+ currentValue: currentChoiceValue(descriptor, settings),
851
973
  }))
852
974
 
853
- await this.drainInternalSetup(
975
+ await this.drainSetup(
854
976
  provider,
855
977
  address,
856
- pendingSetup.filter(isInternal),
857
- visibleSetup.filter((descriptor) => stagedTypes.has(descriptor.type))
978
+ pendingSetup.filter(
979
+ (descriptor) =>
980
+ descriptor.options === null && descriptor.signer === PerpsSigner.SDK
981
+ ),
982
+ stageable.filter((descriptor) => stagedTypes.has(descriptor.type))
858
983
  )
859
984
 
860
985
  return {
861
986
  accountExists: true,
862
987
  setup: actions,
863
- isReady: actions.length === 0,
988
+ isReady:
989
+ actions.length === 0 && checklist.every((item) => item.satisfied),
864
990
  checklist,
865
991
  }
866
992
  }
867
993
 
868
994
  /**
869
- * Resolve setup descriptors already satisfied from the provider's own typed
870
- * account config projection. This catches client-held auth state (e.g. Ondo
871
- * SIWE/JWT) that the backend cannot observe.
995
+ * Project the provider's own typed account config onto its setup steps. This
996
+ * catches client-held auth state (e.g. Ondo SIWE/JWT) that the backend
997
+ * cannot observe, and carries the current value of each choice.
872
998
  */
873
- private async resolveSatisfiedSetup(
999
+ private async resolveSetupSettings(
874
1000
  provider: string,
875
1001
  address: Address
876
- ): Promise<Set<ActionType>> {
1002
+ ): Promise<AccountConfigSetting[]> {
877
1003
  const metadata = await this.getProviderMetadata(provider)
878
1004
  const plugin = this.sdkClient.getProvider(provider)
879
1005
  if (!plugin || typeof plugin.getAccount !== 'function') {
880
- return new Set()
1006
+ return []
881
1007
  }
882
1008
  const account = await plugin.getAccount({ address })
883
- const settings = plugin.projectConfig(
884
- account.config,
885
- metadata.setup,
886
- metadata.options
887
- )
888
- const setupTypes = new Set(
889
- metadata.setup.map((descriptor) => descriptor.type)
890
- )
891
- return new Set(
892
- settings
893
- .filter((setting) => setting.satisfied && setupTypes.has(setting.type))
894
- .map((setting) => setting.type)
895
- )
1009
+ return plugin.projectConfig(account.config, metadata.setup)
896
1010
  }
897
1011
 
898
1012
  /**
899
- * Drain the pending setup steps the provider declares as internal via
900
- * `PerpsProviderPlugin.internalSetupActions`. Each step is built, signed,
1013
+ * Drain the pending `SDK`-signed non-choice steps: each is built, signed,
901
1014
  * and executed in place with the provider's own credentials. A step is
902
1015
  * deferred while any staged user-facing step with a lower `sequence` is
903
1016
  * outstanding — it cannot succeed before its prerequisite (e.g. SET_REFERRAL
@@ -906,17 +1019,15 @@ export class PerpsClient {
906
1019
  * blocks setup — the step stays unsatisfied and is retried on a later
907
1020
  * `checkSetup`.
908
1021
  */
909
- private async drainInternalSetup(
1022
+ private async drainSetup(
910
1023
  provider: string,
911
1024
  address: Address,
912
- internalPending: ProviderAction[],
913
- stagedVisible: ProviderAction[]
1025
+ drainable: SetupAction[],
1026
+ stagedVisible: SetupAction[]
914
1027
  ): Promise<void> {
915
- const sequenceOf = (descriptor: ProviderAction): number =>
916
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
917
- for (const descriptor of internalPending) {
1028
+ for (const descriptor of drainable) {
918
1029
  // A tie (equal sequences, or both absent) declares no order, so the
919
- // internal step defers to the next checkSetup rather than racing.
1030
+ // drained step defers to the next checkSetup rather than racing.
920
1031
  const blocked = stagedVisible.some(
921
1032
  (staged) => sequenceOf(staged) <= sequenceOf(descriptor)
922
1033
  )
@@ -944,7 +1055,7 @@ export class PerpsClient {
944
1055
  }
945
1056
  } catch (error) {
946
1057
  console.debug(
947
- `[perps-sdk] internal setup step '${descriptor.type}' for '${provider}' did not complete; will retry on the next checkSetup.`,
1058
+ `[perps-sdk] setup step '${descriptor.type}' for '${provider}' did not drain; will retry on the next checkSetup.`,
948
1059
  error
949
1060
  )
950
1061
  }
@@ -958,7 +1069,7 @@ export class PerpsClient {
958
1069
  private async buildProviderSetupActions(
959
1070
  provider: string,
960
1071
  address: Address,
961
- descriptors: ProviderAction[]
1072
+ descriptors: SetupAction[]
962
1073
  ): Promise<ActionStep[]> {
963
1074
  const setupPriority = (descriptor: ProviderAction): number =>
964
1075
  descriptor.signingMethod === SigningMethod.SIWE ? 0 : 1
@@ -996,9 +1107,11 @@ export class PerpsClient {
996
1107
 
997
1108
  /**
998
1109
  * Build the unsigned setup `ActionStep`s still outstanding for an account,
999
- * ordered by descriptor `sequence`. The backend filters already-satisfied
1000
- * setup; each plugin contributes its own signer-bearing request fields and
1001
- * any local-state params (e.g. Lighter's known pubkey).
1110
+ * ordered by descriptor `sequence`. Only a `USER`-signed non-choice step is
1111
+ * staged: an `SDK`-signed non-choice step drains inside {@link checkSetup}
1112
+ * and a choice executes the option the user picks. The backend filters
1113
+ * already-satisfied setup; each plugin contributes its own signer-bearing
1114
+ * request fields and any local-state params (e.g. Lighter's known pubkey).
1002
1115
  *
1003
1116
  * @public
1004
1117
  */
@@ -1011,7 +1124,7 @@ export class PerpsClient {
1011
1124
  const actions = await this.buildProviderSetupActions(
1012
1125
  provider,
1013
1126
  address,
1014
- metadata.setup
1127
+ metadata.setup.filter(isStageable)
1015
1128
  )
1016
1129
  return { actions }
1017
1130
  }
@@ -1054,13 +1167,7 @@ export class PerpsClient {
1054
1167
 
1055
1168
  await this.notifyExecuteResults(provider, address, results)
1056
1169
 
1057
- const failure = results.find((r) => !r.success)
1058
- if (failure) {
1059
- throw new PerpsError(
1060
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1061
- failure.error
1062
- )
1063
- }
1170
+ assertAllSucceeded(results)
1064
1171
 
1065
1172
  return { results: { results } }
1066
1173
  }
@@ -1068,25 +1175,23 @@ export class PerpsClient {
1068
1175
  /**
1069
1176
  * Sign and submit one pre-staged setup `ActionStep` end-to-end.
1070
1177
  *
1071
- * The caller is expected to have already obtained the step from a prior
1072
- * {@link checkSetup} call (typically cached by the widget's react-query) —
1073
- * we do NOT refetch. This avoids the double `createAction` round-trip and
1074
- * keeps the nonce that was allocated at staging time committed all the way
1075
- * through submit. If the cached step has gone stale (Lighter's `/nextNonce`
1076
- * advanced underneath us), `executeProviderSetup` will surface a nonce
1077
- * conflict that the caller invalidates on, refetches `checkSetup`, and
1078
- * retries with a fresh step.
1178
+ * `step` and `checklist` must come from the same {@link checkSetup} result.
1179
+ * This preserves the nonce allocated while staging and validates sequence
1180
+ * prerequisites against that same snapshot, without another `createAction`
1181
+ * round-trip.
1079
1182
  *
1080
1183
  * @throws {PerpsError} When the step's action is not in the provider's
1081
- * `setup` descriptors.
1184
+ * `setup` descriptors, or when a lower-`sequence` step on the same
1185
+ * checklist, a choice included, is still unsatisfied.
1082
1186
  * @public
1083
1187
  */
1084
1188
  async executeProviderSetupAction(params: {
1085
1189
  provider: string
1086
1190
  address: Address
1087
1191
  step: ActionStep
1192
+ checklist: SetupChecklistItem[]
1088
1193
  }): Promise<void> {
1089
- const { provider, address, step } = params
1194
+ const { provider, address, step, checklist } = params
1090
1195
 
1091
1196
  const metadata = await this.getProviderMetadata(provider)
1092
1197
  const descriptor = metadata.setup.find((d) => d.type === step.action)
@@ -1097,6 +1202,17 @@ export class PerpsClient {
1097
1202
  )
1098
1203
  }
1099
1204
 
1205
+ const blockingStep = checklist.find(
1206
+ (item) =>
1207
+ !item.satisfied && sequenceOf(item.descriptor) < sequenceOf(descriptor)
1208
+ )
1209
+ if (blockingStep) {
1210
+ throw new PerpsError(
1211
+ PerpsErrorCode.SDKError,
1212
+ `Setup step '${step.action}' for '${provider}' is blocked: '${blockingStep.descriptor.type}' runs first and is not satisfied.`
1213
+ )
1214
+ }
1215
+
1100
1216
  const signed = await this.signProviderSetupAction(provider, address, step)
1101
1217
 
1102
1218
  // A client-executed setup action (Lighter SET_REFERRAL) produces no
@@ -1115,22 +1231,13 @@ export class PerpsClient {
1115
1231
  }
1116
1232
 
1117
1233
  /**
1118
- * Sign and submit a single provider-option change (a `Provider.options`
1119
- * tunable such as Lighter `ACCOUNT_TYPE` or Hyperliquid `ACCOUNT_MODE`)
1120
- * end-to-end, throwing on a venue rejection.
1234
+ * Execute one option of a choice setup step (Lighter `ACCOUNT_TYPE`,
1235
+ * Hyperliquid `ACCOUNT_MODE`) end-to-end with its bound `{ type, params }`,
1236
+ * throwing on a venue rejection.
1121
1237
  *
1122
- * Options are dispatched through the same {@link execute} pipeline as
1123
- * trades, but unlike a trade an option change is a single mandatory action:
1238
+ * The option runs through the same {@link execute} pipeline as a trade, but
1124
1239
  * a per-action `success: false` (returned as a 200 OK) means the user's
1125
- * selection was rejected and must surface, not be silently dropped. This
1126
- * wrapper inspects the result and throws a {@link PerpsError} carrying the
1127
- * venue `error`, giving options the same throw contract that setup has via
1128
- * {@link executeProviderSetupAction}. The only structural difference is that
1129
- * an option carries `params` (the selected value) rather than a pre-staged
1130
- * step.
1131
- *
1132
- * `execute` itself is unchanged — it still returns results without throwing,
1133
- * which the trade hooks rely on for partial-fill handling.
1240
+ * selection was rejected and must surface, not be silently dropped.
1134
1241
  *
1135
1242
  * @throws {PerpsError} Carrying the venue error when any returned result has
1136
1243
  * `success: false`, under that result's `errorCode` when the backend
@@ -1139,20 +1246,56 @@ export class PerpsClient {
1139
1246
  * signer, signing failure).
1140
1247
  * @public
1141
1248
  */
1142
- async executeProviderOption<T extends ActionType>(params: {
1249
+ async executeProviderOption(params: {
1143
1250
  provider: string
1144
1251
  address: Address
1145
- action: T
1146
- params: ActionParamsMap[T]
1252
+ option: SetupOption
1147
1253
  }): Promise<void> {
1148
- const { results } = await this.execute(params)
1149
- const failure = results.find((r) => !r.success)
1150
- if (failure) {
1254
+ const { provider, address, option } = params
1255
+ const { results } = await this.execute({
1256
+ provider,
1257
+ address,
1258
+ action: option.type,
1259
+ params: option.params,
1260
+ })
1261
+ assertAllSucceeded(results)
1262
+ }
1263
+
1264
+ /**
1265
+ * Undo a satisfied setup step by executing its `revoke` action through
1266
+ * create, sign and execute, signed with that action's `Provider.actions`
1267
+ * descriptor. The step stages again on the next {@link checkSetup}.
1268
+ *
1269
+ * @throws {PerpsError} `SDKError` when `step.revoke` is `null` or when
1270
+ * `createAction` stages nothing to revoke; otherwise the errors
1271
+ * {@link executeProviderOption} throws.
1272
+ * @public
1273
+ */
1274
+ async executeProviderRevoke(params: {
1275
+ provider: string
1276
+ address: Address
1277
+ step: SetupAction
1278
+ }): Promise<void> {
1279
+ const { provider, address, step } = params
1280
+ if (step.revoke === null) {
1151
1281
  throw new PerpsError(
1152
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1153
- failure.error
1282
+ PerpsErrorCode.SDKError,
1283
+ `Setup step '${step.type}' for '${provider}' declares no revoke.`
1154
1284
  )
1155
1285
  }
1286
+ const { staged, results } = await this.runAction({
1287
+ provider,
1288
+ address,
1289
+ action: step.revoke,
1290
+ params: {},
1291
+ })
1292
+ if (staged.length === 0) {
1293
+ throw new PerpsError(
1294
+ PerpsErrorCode.SDKError,
1295
+ `Nothing to revoke for setup step '${step.type}' on '${provider}'.`
1296
+ )
1297
+ }
1298
+ assertAllSucceeded(results)
1156
1299
  }
1157
1300
 
1158
1301
  /**
@@ -1333,6 +1476,18 @@ export class PerpsClient {
1333
1476
  * deposit); called as each leg is submitted and confirmed. */
1334
1477
  onProgress?: (progress: SignActionProgress) => void
1335
1478
  }): Promise<ExecuteActionResponse> {
1479
+ const { results } = await this.runAction(params)
1480
+ return { results }
1481
+ }
1482
+
1483
+ /** {@link execute}, also returning the steps `createAction` staged. */
1484
+ private async runAction<T extends ActionType>(params: {
1485
+ provider: string
1486
+ address: Address
1487
+ action: T
1488
+ params: ActionParamsMap[T]
1489
+ onProgress?: (progress: SignActionProgress) => void
1490
+ }): Promise<{ staged: ActionStep[]; results: ActionResult[] }> {
1336
1491
  const { provider, address, action, onProgress } = params
1337
1492
  const metadata = await this.getProviderMetadata(provider)
1338
1493
  const descriptor = findActionDescriptor(metadata, action)
@@ -1363,7 +1518,7 @@ export class PerpsClient {
1363
1518
  // token-authenticated venue mutations), leaving no backend-bound step. With
1364
1519
  // nothing to submit, skip the `/executeAction` hop.
1365
1520
  if (signedActions.length === 0) {
1366
- return { results: [] }
1521
+ return { staged: actions, results: [] }
1367
1522
  }
1368
1523
 
1369
1524
  const response = await executeAction(this.sdkClient, {
@@ -1380,7 +1535,7 @@ export class PerpsClient {
1380
1535
 
1381
1536
  await this.notifyExecuteResults(provider, address, results)
1382
1537
 
1383
- return { results }
1538
+ return { staged: actions, results }
1384
1539
  }
1385
1540
 
1386
1541
  /**