@lifi/perps-sdk 18.0.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 (67) hide show
  1. package/README.md +12 -0
  2. package/dist/cjs/client/PerpsClient.d.ts +15 -8
  3. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  4. package/dist/cjs/client/PerpsClient.js +145 -52
  5. package/dist/cjs/client/PerpsClient.js.map +1 -1
  6. package/dist/cjs/index.d.ts +4 -4
  7. package/dist/cjs/index.d.ts.map +1 -1
  8. package/dist/cjs/index.js +2 -1
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/types/api.d.ts +8 -2
  11. package/dist/cjs/types/api.d.ts.map +1 -1
  12. package/dist/cjs/types/provider.d.ts +9 -6
  13. package/dist/cjs/types/provider.d.ts.map +1 -1
  14. package/dist/cjs/types/withdrawal.d.ts +11 -1
  15. package/dist/cjs/types/withdrawal.d.ts.map +1 -1
  16. package/dist/cjs/utils/setupActions.d.ts +4 -2
  17. package/dist/cjs/utils/setupActions.d.ts.map +1 -1
  18. package/dist/cjs/utils/setupActions.js +9 -1
  19. package/dist/cjs/utils/setupActions.js.map +1 -1
  20. package/dist/cjs/utils/switchChain.js +1 -1
  21. package/dist/cjs/utils/switchChain.js.map +1 -1
  22. package/dist/cjs/version.d.ts +1 -1
  23. package/dist/cjs/version.js +1 -1
  24. package/dist/esm/client/PerpsClient.d.ts +74 -55
  25. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  26. package/dist/esm/client/PerpsClient.js +218 -100
  27. package/dist/esm/client/PerpsClient.js.map +1 -1
  28. package/dist/esm/index.d.ts +4 -4
  29. package/dist/esm/index.d.ts.map +1 -1
  30. package/dist/esm/index.js +1 -1
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/types/api.d.ts +40 -14
  33. package/dist/esm/types/api.d.ts.map +1 -1
  34. package/dist/esm/types/provider.d.ts +39 -28
  35. package/dist/esm/types/provider.d.ts.map +1 -1
  36. package/dist/esm/types/withdrawal.d.ts +30 -1
  37. package/dist/esm/types/withdrawal.d.ts.map +1 -1
  38. package/dist/esm/utils/setupActions.d.ts +17 -8
  39. package/dist/esm/utils/setupActions.d.ts.map +1 -1
  40. package/dist/esm/utils/setupActions.js +20 -7
  41. package/dist/esm/utils/setupActions.js.map +1 -1
  42. package/dist/esm/utils/switchChain.js +1 -1
  43. package/dist/esm/utils/switchChain.js.map +1 -1
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/dist/types/client/PerpsClient.d.ts +74 -55
  47. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  48. package/dist/types/index.d.ts +4 -4
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/types/types/api.d.ts +40 -14
  51. package/dist/types/types/api.d.ts.map +1 -1
  52. package/dist/types/types/provider.d.ts +39 -28
  53. package/dist/types/types/provider.d.ts.map +1 -1
  54. package/dist/types/types/withdrawal.d.ts +30 -1
  55. package/dist/types/types/withdrawal.d.ts.map +1 -1
  56. package/dist/types/utils/setupActions.d.ts +17 -8
  57. package/dist/types/utils/setupActions.d.ts.map +1 -1
  58. package/dist/types/version.d.ts +1 -1
  59. package/package.json +2 -2
  60. package/src/client/PerpsClient.ts +303 -130
  61. package/src/index.ts +9 -1
  62. package/src/types/api.ts +42 -14
  63. package/src/types/provider.ts +44 -30
  64. package/src/types/withdrawal.ts +33 -1
  65. package/src/utils/setupActions.ts +27 -13
  66. package/src/utils/switchChain.ts +1 -1
  67. 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,
@@ -55,6 +59,7 @@ import type {
55
59
  GetPortfolioHistoryParams,
56
60
  GetSetupParams,
57
61
  GetWithdrawableBalancesParams,
62
+ GetWithdrawFlowParams,
58
63
  ModifyOrdersParams,
59
64
  PerpsClientOptions,
60
65
  PlaceOrderParams,
@@ -62,6 +67,7 @@ import type {
62
67
  PlaceTwapOrderParams,
63
68
  ProviderSetup,
64
69
  SendAssetActionParams,
70
+ SetupChecklistItem,
65
71
  SubmitOnboardingParams,
66
72
  WithdrawParams,
67
73
  } from '../types/api.js'
@@ -78,7 +84,8 @@ import type {
78
84
  SignActionProgress,
79
85
  SignActionsContext,
80
86
  } from '../types/provider.js'
81
- import type { WithdrawableBalance } from '../types/withdrawal.js'
87
+ import type { WithdrawableBalance, WithdrawFlow } from '../types/withdrawal.js'
88
+ import { isUserFacingSetupStep } from '../utils/setupActions.js'
82
89
  import { signTypedDataWithSigner } from '../utils/signTypedData.js'
83
90
  import {
84
91
  eip712DomainChainId,
@@ -88,6 +95,127 @@ import {
88
95
  import { createPerpsClient } from './createPerpsClient.js'
89
96
  import { requireProvider as resolveProvider } from './requireProvider.js'
90
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
+
91
219
  function isNonNegativeDecimal(value: string): boolean {
92
220
  try {
93
221
  return new Big(value).gte(0)
@@ -104,11 +232,9 @@ function findActionDescriptor(
104
232
  metadata: Provider,
105
233
  action: ActionType
106
234
  ): ProviderAction {
107
- const descriptor = [
108
- ...metadata.setup,
109
- ...metadata.options,
110
- ...metadata.actions,
111
- ].find((d) => d.type === action)
235
+ const descriptor = [...metadata.setup, ...metadata.actions].find(
236
+ (d) => d.type === action
237
+ )
112
238
  if (!descriptor) {
113
239
  throw new PerpsError(
114
240
  PerpsErrorCode.SDKError,
@@ -170,7 +296,7 @@ export class PerpsClient {
170
296
 
171
297
  /**
172
298
  * Set or update the end-user's wallet. Used whenever an action's descriptor
173
- * names the user wallet in its `signers` list. Pass undefined to clear.
299
+ * names `PerpsSigner.USER` as its `signer`. Pass undefined to clear.
174
300
  *
175
301
  * @public
176
302
  */
@@ -204,17 +330,15 @@ export class PerpsClient {
204
330
  }
205
331
 
206
332
  private async getProviderMetadata(provider: string): Promise<Provider> {
207
- const cached = this.providerMetadataCache.get(provider)
208
- if (cached) {
209
- return cached
210
- }
211
-
212
- const { providers } = await getProviders(this.sdkClient)
213
- for (const d of providers) {
214
- 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)
215
340
  }
216
341
 
217
- const metadata = this.providerMetadataCache.get(provider)
218
342
  if (!metadata) {
219
343
  const error = new PerpsError(
220
344
  PerpsErrorCode.SDKError,
@@ -223,6 +347,7 @@ export class PerpsClient {
223
347
  error.tool = '@lifi/perps-sdk'
224
348
  throw error
225
349
  }
350
+ assertSetupContract(metadata)
226
351
  return metadata
227
352
  }
228
353
 
@@ -255,7 +380,7 @@ export class PerpsClient {
255
380
  * Ask the provider plugin for the signer-bearing wire fields of `action` —
256
381
  * the on-wire `signerAddress` and any signer-derived params (e.g.
257
382
  * Hyperliquid's `agentAddress` for `APPROVE_AGENT`). Forwards the descriptor's
258
- * `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
259
384
  * plugin signs as the user or with a non-EVM credential. Core constructs no
260
385
  * `signerAddress` itself; signer identity is plugin-owned.
261
386
  */
@@ -271,17 +396,17 @@ export class PerpsClient {
271
396
  return plugin.resolveActionRequest(
272
397
  descriptor.type,
273
398
  address,
274
- descriptor.signers
399
+ descriptor.signer
275
400
  )
276
401
  }
277
402
 
278
403
  /**
279
404
  * Delegate signing of `actions` to the provider plugin, one call per
280
405
  * consecutive run of steps sharing a descriptor. Each call carries that
281
- * descriptor's `signingMethod` and `signers`, so a mixed batch signs every
406
+ * descriptor's `signingMethod` and `signer`, so a mixed batch signs every
282
407
  * step with the signer its own descriptor declares; the signed steps come
283
408
  * back in the batch's original order. The plugin owns every signing arm and
284
- * 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
285
410
  * {@link SignActionsContext} when an arm signs as the user.
286
411
  *
287
412
  * @throws {PerpsError} When a step's action is not declared by the provider,
@@ -404,9 +529,9 @@ export class PerpsClient {
404
529
 
405
530
  /**
406
531
  * Assemble the per-call context the provider plugin needs in order to sign:
407
- * the end-user's wallet and the descriptor's declared `signers`. Core
408
- * forwards `signers` as data so the plugin can pick WHO signs; it does not
409
- * 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
410
535
  * keypair, Lighter's API key) are resolved inside the provider's
411
536
  * `signActions`, not threaded through here.
412
537
  *
@@ -424,7 +549,7 @@ export class PerpsClient {
424
549
  userWallet?: PerpsClientSigner,
425
550
  onProgress?: (progress: SignActionProgress) => void
426
551
  ): SignActionsContext {
427
- const ctx: SignActionsContext = { signers: descriptor.signers }
552
+ const ctx: SignActionsContext = { signer: descriptor.signer }
428
553
  if (onProgress !== undefined) {
429
554
  ctx.onProgress = onProgress
430
555
  }
@@ -500,7 +625,7 @@ export class PerpsClient {
500
625
  /**
501
626
  * Fetch the user's account state from the backend and attach the
502
627
  * SDK-projected `settings` array — one `AccountConfigSetting` per
503
- * descriptor on `Provider.setup` + `Provider.options`. Callers read
628
+ * descriptor on `Provider.setup`. Callers read
504
629
  * `result.settings` directly without re-deriving values from the typed
505
630
  * `AccountConfig`.
506
631
  *
@@ -517,11 +642,7 @@ export class PerpsClient {
517
642
  fetchAccount(this.sdkClient, params),
518
643
  this.getProviderMetadata(params.provider),
519
644
  ])
520
- const settings = plugin.projectConfig(
521
- response.config,
522
- metadata.setup,
523
- metadata.options
524
- )
645
+ const settings = plugin.projectConfig(response.config, metadata.setup)
525
646
  return { ...response, settings }
526
647
  }
527
648
 
@@ -661,6 +782,23 @@ export class PerpsClient {
661
782
  return plugin.getDepositFlow?.({ address: params.address })
662
783
  }
663
784
 
785
+ /**
786
+ * The withdrawal flow for `params.address` at `params.provider`, delegated to
787
+ * the provider plugin — the withdrawal destination and any setup gate before
788
+ * it are venue-owned.
789
+ *
790
+ * @returns `undefined` when the registered plugin declares no withdrawal flow.
791
+ * @throws {PerpsError} When the provider plugin is not registered, or the
792
+ * plugin's flow resolution fails.
793
+ * @public
794
+ */
795
+ async getWithdrawFlow(
796
+ params: GetWithdrawFlowParams
797
+ ): Promise<WithdrawFlow | undefined> {
798
+ const plugin = this.requireProvider(params.provider)
799
+ return plugin.getWithdrawFlow?.({ address: params.address })
800
+ }
801
+
664
802
  /**
665
803
  * The `(asset, route)` selections `params.address` can actually withdraw at
666
804
  * `params.provider`. The venue owns how its balances split across routes;
@@ -755,15 +893,17 @@ export class PerpsClient {
755
893
  }
756
894
 
757
895
  /**
758
- * Return the unsatisfied entries on `Provider.setup` for this account as a
759
- * flat, self-describing list. Trading is gated on `isReady === true`.
760
- * `checklist` carries the renderable onboarding list: every USER-signed
761
- * descriptor with its satisfied state, with not-required conditional steps
762
- * 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.
763
901
  *
764
- * `Provider.options` descriptors are NEVER returned here — options are
765
- * post-setup tunables and never gate trading. Option state is surfaced
766
- * 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.
767
907
  *
768
908
  * @public
769
909
  */
@@ -789,19 +929,15 @@ export class PerpsClient {
789
929
  }
790
930
  }
791
931
 
792
- const satisfiedSetup = await this.resolveSatisfiedSetup(provider, address)
932
+ const settings = await this.resolveSetupSettings(provider, address)
933
+ const satisfiedSetup = satisfiedTypes(settings)
793
934
  const pendingSetup = metadata.setup.filter(
794
935
  (descriptor) => !satisfiedSetup.has(descriptor.type)
795
936
  )
796
937
 
797
938
  const plugin = this.sdkClient.getProvider(provider)
798
- const internalTypes = new Set(plugin?.internalSetupActions ?? [])
799
- const isInternal = (descriptor: ProviderAction): boolean =>
800
- internalTypes.has(descriptor.type) &&
801
- !descriptor.signers.includes(PerpsSigner.USER)
802
- const visibleSetup = pendingSetup.filter(
803
- (descriptor) => !isInternal(descriptor)
804
- )
939
+
940
+ const stageable = pendingSetup.filter(isStageable)
805
941
 
806
942
  // The backend filters already-satisfied setup actions and returns typed
807
943
  // data for those still outstanding; each plugin contributes its own
@@ -809,7 +945,7 @@ export class PerpsClient {
809
945
  const actions = await this.buildProviderSetupActions(
810
946
  provider,
811
947
  address,
812
- visibleSetup
948
+ stageable
813
949
  )
814
950
 
815
951
  // A staged step is one the build produced actions for — a pending
@@ -817,69 +953,64 @@ export class PerpsClient {
817
953
  // (backend-gated) or not applicable to this account.
818
954
  const stagedTypes = new Set(actions.map((step) => step.action))
819
955
  const conditionalTypes = new Set(plugin?.conditionalSetupActions ?? [])
820
- const sequenceOf = (descriptor: ProviderAction): number =>
821
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
822
956
  const checklist = metadata.setup
823
- .filter((descriptor) => descriptor.signers.includes(PerpsSigner.USER))
957
+ .filter(isUserFacingSetupStep)
824
958
  .filter(
825
959
  (descriptor) =>
960
+ descriptor.options !== null ||
826
961
  !conditionalTypes.has(descriptor.type) ||
827
962
  stagedTypes.has(descriptor.type)
828
963
  )
829
964
  .sort((a, b) => sequenceOf(a) - sequenceOf(b))
830
965
  .map((descriptor) => ({
831
966
  descriptor,
832
- 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),
833
973
  }))
834
974
 
835
- await this.drainInternalSetup(
975
+ await this.drainSetup(
836
976
  provider,
837
977
  address,
838
- pendingSetup.filter(isInternal),
839
- 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))
840
983
  )
841
984
 
842
985
  return {
843
986
  accountExists: true,
844
987
  setup: actions,
845
- isReady: actions.length === 0,
988
+ isReady:
989
+ actions.length === 0 && checklist.every((item) => item.satisfied),
846
990
  checklist,
847
991
  }
848
992
  }
849
993
 
850
994
  /**
851
- * Resolve setup descriptors already satisfied from the provider's own typed
852
- * account config projection. This catches client-held auth state (e.g. Ondo
853
- * 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.
854
998
  */
855
- private async resolveSatisfiedSetup(
999
+ private async resolveSetupSettings(
856
1000
  provider: string,
857
1001
  address: Address
858
- ): Promise<Set<ActionType>> {
1002
+ ): Promise<AccountConfigSetting[]> {
859
1003
  const metadata = await this.getProviderMetadata(provider)
860
1004
  const plugin = this.sdkClient.getProvider(provider)
861
1005
  if (!plugin || typeof plugin.getAccount !== 'function') {
862
- return new Set()
1006
+ return []
863
1007
  }
864
1008
  const account = await plugin.getAccount({ address })
865
- const settings = plugin.projectConfig(
866
- account.config,
867
- metadata.setup,
868
- metadata.options
869
- )
870
- const setupTypes = new Set(
871
- metadata.setup.map((descriptor) => descriptor.type)
872
- )
873
- return new Set(
874
- settings
875
- .filter((setting) => setting.satisfied && setupTypes.has(setting.type))
876
- .map((setting) => setting.type)
877
- )
1009
+ return plugin.projectConfig(account.config, metadata.setup)
878
1010
  }
879
1011
 
880
1012
  /**
881
- * Drain the pending setup steps the provider declares as internal via
882
- * `PerpsProviderPlugin.internalSetupActions`. Each step is built, signed,
1013
+ * Drain the pending `SDK`-signed non-choice steps: each is built, signed,
883
1014
  * and executed in place with the provider's own credentials. A step is
884
1015
  * deferred while any staged user-facing step with a lower `sequence` is
885
1016
  * outstanding — it cannot succeed before its prerequisite (e.g. SET_REFERRAL
@@ -888,17 +1019,15 @@ export class PerpsClient {
888
1019
  * blocks setup — the step stays unsatisfied and is retried on a later
889
1020
  * `checkSetup`.
890
1021
  */
891
- private async drainInternalSetup(
1022
+ private async drainSetup(
892
1023
  provider: string,
893
1024
  address: Address,
894
- internalPending: ProviderAction[],
895
- stagedVisible: ProviderAction[]
1025
+ drainable: SetupAction[],
1026
+ stagedVisible: SetupAction[]
896
1027
  ): Promise<void> {
897
- const sequenceOf = (descriptor: ProviderAction): number =>
898
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
899
- for (const descriptor of internalPending) {
1028
+ for (const descriptor of drainable) {
900
1029
  // A tie (equal sequences, or both absent) declares no order, so the
901
- // internal step defers to the next checkSetup rather than racing.
1030
+ // drained step defers to the next checkSetup rather than racing.
902
1031
  const blocked = stagedVisible.some(
903
1032
  (staged) => sequenceOf(staged) <= sequenceOf(descriptor)
904
1033
  )
@@ -926,7 +1055,7 @@ export class PerpsClient {
926
1055
  }
927
1056
  } catch (error) {
928
1057
  console.debug(
929
- `[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.`,
930
1059
  error
931
1060
  )
932
1061
  }
@@ -940,7 +1069,7 @@ export class PerpsClient {
940
1069
  private async buildProviderSetupActions(
941
1070
  provider: string,
942
1071
  address: Address,
943
- descriptors: ProviderAction[]
1072
+ descriptors: SetupAction[]
944
1073
  ): Promise<ActionStep[]> {
945
1074
  const setupPriority = (descriptor: ProviderAction): number =>
946
1075
  descriptor.signingMethod === SigningMethod.SIWE ? 0 : 1
@@ -978,9 +1107,11 @@ export class PerpsClient {
978
1107
 
979
1108
  /**
980
1109
  * Build the unsigned setup `ActionStep`s still outstanding for an account,
981
- * ordered by descriptor `sequence`. The backend filters already-satisfied
982
- * setup; each plugin contributes its own signer-bearing request fields and
983
- * 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).
984
1115
  *
985
1116
  * @public
986
1117
  */
@@ -993,7 +1124,7 @@ export class PerpsClient {
993
1124
  const actions = await this.buildProviderSetupActions(
994
1125
  provider,
995
1126
  address,
996
- metadata.setup
1127
+ metadata.setup.filter(isStageable)
997
1128
  )
998
1129
  return { actions }
999
1130
  }
@@ -1036,13 +1167,7 @@ export class PerpsClient {
1036
1167
 
1037
1168
  await this.notifyExecuteResults(provider, address, results)
1038
1169
 
1039
- const failure = results.find((r) => !r.success)
1040
- if (failure) {
1041
- throw new PerpsError(
1042
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1043
- failure.error
1044
- )
1045
- }
1170
+ assertAllSucceeded(results)
1046
1171
 
1047
1172
  return { results: { results } }
1048
1173
  }
@@ -1050,25 +1175,23 @@ export class PerpsClient {
1050
1175
  /**
1051
1176
  * Sign and submit one pre-staged setup `ActionStep` end-to-end.
1052
1177
  *
1053
- * The caller is expected to have already obtained the step from a prior
1054
- * {@link checkSetup} call (typically cached by the widget's react-query) —
1055
- * we do NOT refetch. This avoids the double `createAction` round-trip and
1056
- * keeps the nonce that was allocated at staging time committed all the way
1057
- * through submit. If the cached step has gone stale (Lighter's `/nextNonce`
1058
- * advanced underneath us), `executeProviderSetup` will surface a nonce
1059
- * conflict that the caller invalidates on, refetches `checkSetup`, and
1060
- * 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.
1061
1182
  *
1062
1183
  * @throws {PerpsError} When the step's action is not in the provider's
1063
- * `setup` descriptors.
1184
+ * `setup` descriptors, or when a lower-`sequence` step on the same
1185
+ * checklist, a choice included, is still unsatisfied.
1064
1186
  * @public
1065
1187
  */
1066
1188
  async executeProviderSetupAction(params: {
1067
1189
  provider: string
1068
1190
  address: Address
1069
1191
  step: ActionStep
1192
+ checklist: SetupChecklistItem[]
1070
1193
  }): Promise<void> {
1071
- const { provider, address, step } = params
1194
+ const { provider, address, step, checklist } = params
1072
1195
 
1073
1196
  const metadata = await this.getProviderMetadata(provider)
1074
1197
  const descriptor = metadata.setup.find((d) => d.type === step.action)
@@ -1079,6 +1202,17 @@ export class PerpsClient {
1079
1202
  )
1080
1203
  }
1081
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
+
1082
1216
  const signed = await this.signProviderSetupAction(provider, address, step)
1083
1217
 
1084
1218
  // A client-executed setup action (Lighter SET_REFERRAL) produces no
@@ -1097,22 +1231,13 @@ export class PerpsClient {
1097
1231
  }
1098
1232
 
1099
1233
  /**
1100
- * Sign and submit a single provider-option change (a `Provider.options`
1101
- * tunable such as Lighter `ACCOUNT_TYPE` or Hyperliquid `ACCOUNT_MODE`)
1102
- * 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.
1103
1237
  *
1104
- * Options are dispatched through the same {@link execute} pipeline as
1105
- * 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
1106
1239
  * a per-action `success: false` (returned as a 200 OK) means the user's
1107
- * selection was rejected and must surface, not be silently dropped. This
1108
- * wrapper inspects the result and throws a {@link PerpsError} carrying the
1109
- * venue `error`, giving options the same throw contract that setup has via
1110
- * {@link executeProviderSetupAction}. The only structural difference is that
1111
- * an option carries `params` (the selected value) rather than a pre-staged
1112
- * step.
1113
- *
1114
- * `execute` itself is unchanged — it still returns results without throwing,
1115
- * which the trade hooks rely on for partial-fill handling.
1240
+ * selection was rejected and must surface, not be silently dropped.
1116
1241
  *
1117
1242
  * @throws {PerpsError} Carrying the venue error when any returned result has
1118
1243
  * `success: false`, under that result's `errorCode` when the backend
@@ -1121,20 +1246,56 @@ export class PerpsClient {
1121
1246
  * signer, signing failure).
1122
1247
  * @public
1123
1248
  */
1124
- async executeProviderOption<T extends ActionType>(params: {
1249
+ async executeProviderOption(params: {
1125
1250
  provider: string
1126
1251
  address: Address
1127
- action: T
1128
- params: ActionParamsMap[T]
1252
+ option: SetupOption
1129
1253
  }): Promise<void> {
1130
- const { results } = await this.execute(params)
1131
- const failure = results.find((r) => !r.success)
1132
- 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) {
1133
1281
  throw new PerpsError(
1134
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1135
- failure.error
1282
+ PerpsErrorCode.SDKError,
1283
+ `Setup step '${step.type}' for '${provider}' declares no revoke.`
1136
1284
  )
1137
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)
1138
1299
  }
1139
1300
 
1140
1301
  /**
@@ -1315,6 +1476,18 @@ export class PerpsClient {
1315
1476
  * deposit); called as each leg is submitted and confirmed. */
1316
1477
  onProgress?: (progress: SignActionProgress) => void
1317
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[] }> {
1318
1491
  const { provider, address, action, onProgress } = params
1319
1492
  const metadata = await this.getProviderMetadata(provider)
1320
1493
  const descriptor = findActionDescriptor(metadata, action)
@@ -1345,7 +1518,7 @@ export class PerpsClient {
1345
1518
  // token-authenticated venue mutations), leaving no backend-bound step. With
1346
1519
  // nothing to submit, skip the `/executeAction` hop.
1347
1520
  if (signedActions.length === 0) {
1348
- return { results: [] }
1521
+ return { staged: actions, results: [] }
1349
1522
  }
1350
1523
 
1351
1524
  const response = await executeAction(this.sdkClient, {
@@ -1362,7 +1535,7 @@ export class PerpsClient {
1362
1535
 
1363
1536
  await this.notifyExecuteResults(provider, address, results)
1364
1537
 
1365
- return { results }
1538
+ return { staged: actions, results }
1366
1539
  }
1367
1540
 
1368
1541
  /**