@lifi/perps-sdk 18.1.0 → 20.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 (162) hide show
  1. package/README.md +15 -5
  2. package/dist/cjs/client/PerpsClient.d.ts +14 -8
  3. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  4. package/dist/cjs/client/PerpsClient.js +145 -53
  5. package/dist/cjs/client/PerpsClient.js.map +1 -1
  6. package/dist/cjs/client/createPerpsClient.d.ts +1 -6
  7. package/dist/cjs/client/createPerpsClient.d.ts.map +1 -1
  8. package/dist/cjs/client/createPerpsClient.js +1 -14
  9. package/dist/cjs/client/createPerpsClient.js.map +1 -1
  10. package/dist/cjs/index.d.ts +6 -4
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +11 -3
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/services/getOhlcv.d.ts.map +1 -1
  15. package/dist/cjs/services/getOhlcv.js.map +1 -1
  16. package/dist/cjs/types/api.d.ts +7 -6
  17. package/dist/cjs/types/api.d.ts.map +1 -1
  18. package/dist/cjs/types/config.d.ts +0 -9
  19. package/dist/cjs/types/config.d.ts.map +1 -1
  20. package/dist/cjs/types/provider.d.ts +5 -6
  21. package/dist/cjs/types/provider.d.ts.map +1 -1
  22. package/dist/cjs/types/withdrawal.d.ts +4 -2
  23. package/dist/cjs/types/withdrawal.d.ts.map +1 -1
  24. package/dist/cjs/utils/calculations.d.ts.map +1 -1
  25. package/dist/cjs/utils/calculations.js +107 -38
  26. package/dist/cjs/utils/calculations.js.map +1 -1
  27. package/dist/cjs/utils/decimal.d.ts +4 -0
  28. package/dist/cjs/utils/decimal.d.ts.map +1 -0
  29. package/dist/cjs/utils/decimal.js +14 -0
  30. package/dist/cjs/utils/decimal.js.map +1 -0
  31. package/dist/cjs/utils/math.d.ts +4 -0
  32. package/dist/cjs/utils/math.d.ts.map +1 -0
  33. package/dist/cjs/utils/math.js +8 -0
  34. package/dist/cjs/utils/math.js.map +1 -0
  35. package/dist/cjs/utils/parse.d.ts +1 -0
  36. package/dist/cjs/utils/parse.d.ts.map +1 -1
  37. package/dist/cjs/utils/parse.js +15 -0
  38. package/dist/cjs/utils/parse.js.map +1 -1
  39. package/dist/cjs/utils/positionMath.d.ts +5 -0
  40. package/dist/cjs/utils/positionMath.d.ts.map +1 -1
  41. package/dist/cjs/utils/positionMath.js +53 -6
  42. package/dist/cjs/utils/positionMath.js.map +1 -1
  43. package/dist/cjs/utils/setupActions.d.ts +4 -2
  44. package/dist/cjs/utils/setupActions.d.ts.map +1 -1
  45. package/dist/cjs/utils/setupActions.js +9 -1
  46. package/dist/cjs/utils/setupActions.js.map +1 -1
  47. package/dist/cjs/utils/switchChain.js +1 -1
  48. package/dist/cjs/utils/switchChain.js.map +1 -1
  49. package/dist/cjs/version.d.ts +1 -1
  50. package/dist/cjs/version.js +1 -1
  51. package/dist/cjs/websocket/PerpsWsClient.d.ts +0 -1
  52. package/dist/cjs/websocket/PerpsWsClient.d.ts.map +1 -1
  53. package/dist/cjs/websocket/PerpsWsClient.js +0 -7
  54. package/dist/cjs/websocket/PerpsWsClient.js.map +1 -1
  55. package/dist/esm/client/PerpsClient.d.ts +70 -61
  56. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  57. package/dist/esm/client/PerpsClient.js +215 -107
  58. package/dist/esm/client/PerpsClient.js.map +1 -1
  59. package/dist/esm/client/createPerpsClient.d.ts +1 -12
  60. package/dist/esm/client/createPerpsClient.d.ts.map +1 -1
  61. package/dist/esm/client/createPerpsClient.js +1 -19
  62. package/dist/esm/client/createPerpsClient.js.map +1 -1
  63. package/dist/esm/client/requireProvider.d.ts +1 -1
  64. package/dist/esm/client/requireProvider.js +1 -1
  65. package/dist/esm/index.d.ts +6 -4
  66. package/dist/esm/index.d.ts.map +1 -1
  67. package/dist/esm/index.js +5 -3
  68. package/dist/esm/index.js.map +1 -1
  69. package/dist/esm/services/getOhlcv.d.ts +1 -0
  70. package/dist/esm/services/getOhlcv.d.ts.map +1 -1
  71. package/dist/esm/services/getOhlcv.js.map +1 -1
  72. package/dist/esm/types/api.d.ts +39 -36
  73. package/dist/esm/types/api.d.ts.map +1 -1
  74. package/dist/esm/types/config.d.ts +0 -27
  75. package/dist/esm/types/config.d.ts.map +1 -1
  76. package/dist/esm/types/provider.d.ts +28 -33
  77. package/dist/esm/types/provider.d.ts.map +1 -1
  78. package/dist/esm/types/withdrawal.d.ts +24 -15
  79. package/dist/esm/types/withdrawal.d.ts.map +1 -1
  80. package/dist/esm/utils/calculations.d.ts +3 -2
  81. package/dist/esm/utils/calculations.d.ts.map +1 -1
  82. package/dist/esm/utils/calculations.js +109 -39
  83. package/dist/esm/utils/calculations.js.map +1 -1
  84. package/dist/esm/utils/decimal.d.ts +9 -0
  85. package/dist/esm/utils/decimal.d.ts.map +1 -0
  86. package/dist/esm/utils/decimal.js +12 -0
  87. package/dist/esm/utils/decimal.js.map +1 -0
  88. package/dist/esm/utils/math.d.ts +6 -0
  89. package/dist/esm/utils/math.d.ts.map +1 -0
  90. package/dist/esm/utils/math.js +5 -0
  91. package/dist/esm/utils/math.js.map +1 -0
  92. package/dist/esm/utils/parse.d.ts +7 -0
  93. package/dist/esm/utils/parse.d.ts.map +1 -1
  94. package/dist/esm/utils/parse.js +20 -0
  95. package/dist/esm/utils/parse.js.map +1 -1
  96. package/dist/esm/utils/positionMath.d.ts +13 -0
  97. package/dist/esm/utils/positionMath.d.ts.map +1 -1
  98. package/dist/esm/utils/positionMath.js +60 -6
  99. package/dist/esm/utils/positionMath.js.map +1 -1
  100. package/dist/esm/utils/setupActions.d.ts +17 -8
  101. package/dist/esm/utils/setupActions.d.ts.map +1 -1
  102. package/dist/esm/utils/setupActions.js +20 -7
  103. package/dist/esm/utils/setupActions.js.map +1 -1
  104. package/dist/esm/utils/switchChain.js +1 -1
  105. package/dist/esm/utils/switchChain.js.map +1 -1
  106. package/dist/esm/version.d.ts +1 -1
  107. package/dist/esm/version.js +1 -1
  108. package/dist/esm/websocket/PerpsWsClient.d.ts +0 -7
  109. package/dist/esm/websocket/PerpsWsClient.d.ts.map +1 -1
  110. package/dist/esm/websocket/PerpsWsClient.js +0 -7
  111. package/dist/esm/websocket/PerpsWsClient.js.map +1 -1
  112. package/dist/types/client/PerpsClient.d.ts +70 -61
  113. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  114. package/dist/types/client/createPerpsClient.d.ts +1 -12
  115. package/dist/types/client/createPerpsClient.d.ts.map +1 -1
  116. package/dist/types/client/requireProvider.d.ts +1 -1
  117. package/dist/types/index.d.ts +6 -4
  118. package/dist/types/index.d.ts.map +1 -1
  119. package/dist/types/services/getOhlcv.d.ts +1 -0
  120. package/dist/types/services/getOhlcv.d.ts.map +1 -1
  121. package/dist/types/types/api.d.ts +39 -36
  122. package/dist/types/types/api.d.ts.map +1 -1
  123. package/dist/types/types/config.d.ts +0 -27
  124. package/dist/types/types/config.d.ts.map +1 -1
  125. package/dist/types/types/provider.d.ts +28 -33
  126. package/dist/types/types/provider.d.ts.map +1 -1
  127. package/dist/types/types/withdrawal.d.ts +24 -15
  128. package/dist/types/types/withdrawal.d.ts.map +1 -1
  129. package/dist/types/utils/calculations.d.ts +3 -2
  130. package/dist/types/utils/calculations.d.ts.map +1 -1
  131. package/dist/types/utils/decimal.d.ts +9 -0
  132. package/dist/types/utils/decimal.d.ts.map +1 -0
  133. package/dist/types/utils/math.d.ts +6 -0
  134. package/dist/types/utils/math.d.ts.map +1 -0
  135. package/dist/types/utils/parse.d.ts +7 -0
  136. package/dist/types/utils/parse.d.ts.map +1 -1
  137. package/dist/types/utils/positionMath.d.ts +13 -0
  138. package/dist/types/utils/positionMath.d.ts.map +1 -1
  139. package/dist/types/utils/setupActions.d.ts +17 -8
  140. package/dist/types/utils/setupActions.d.ts.map +1 -1
  141. package/dist/types/version.d.ts +1 -1
  142. package/dist/types/websocket/PerpsWsClient.d.ts +0 -7
  143. package/dist/types/websocket/PerpsWsClient.d.ts.map +1 -1
  144. package/package.json +2 -2
  145. package/src/client/PerpsClient.ts +296 -137
  146. package/src/client/createPerpsClient.ts +4 -32
  147. package/src/client/requireProvider.ts +1 -1
  148. package/src/index.ts +8 -5
  149. package/src/services/getOhlcv.ts +1 -0
  150. package/src/types/api.ts +40 -40
  151. package/src/types/config.ts +0 -30
  152. package/src/types/provider.ts +29 -36
  153. package/src/types/withdrawal.ts +24 -15
  154. package/src/utils/calculations.ts +114 -43
  155. package/src/utils/decimal.ts +13 -0
  156. package/src/utils/math.ts +7 -0
  157. package/src/utils/parse.ts +25 -0
  158. package/src/utils/positionMath.ts +66 -6
  159. package/src/utils/setupActions.ts +27 -13
  160. package/src/utils/switchChain.ts +1 -1
  161. package/src/version.ts +1 -1
  162. package/src/websocket/PerpsWsClient.ts +0 -19
@@ -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,30 +642,27 @@ 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
 
529
649
  /**
530
- * The user's current venue-side settings for a market — the margin mode
531
- * and leverage the next order on it will use. Resolves `undefined` when
532
- * the venue exposes no readable setting for the market (or the provider
533
- * has no such read at all).
650
+ * The user's current venue-side settings for a perps market — the margin
651
+ * mode and leverage the next order on it will use. Resolves the venue value
652
+ * or throws.
534
653
  *
535
- * @throws {PerpsError} When the provider plugin is not registered.
654
+ * @throws {PerpsError} When the provider plugin is not registered,
655
+ * `ValidationError` for a spot market, or any other `PerpsError` when the
656
+ * venue read fails or carries no usable value.
536
657
  * @public
537
658
  */
538
659
  async getMarketSettings(params: {
539
660
  provider: string
540
661
  address: Address
541
662
  market: MarketRef
542
- }): Promise<MarketSettings | undefined> {
663
+ }): Promise<MarketSettings> {
543
664
  const plugin = this.requireProvider(params.provider)
544
- return plugin.getMarketSettings?.({
665
+ return plugin.getMarketSettings({
545
666
  address: params.address,
546
667
  market: params.market,
547
668
  })
@@ -690,7 +811,7 @@ export class PerpsClient {
690
811
  * @returns `undefined` when the registered plugin declares no withdrawable
691
812
  * read.
692
813
  * @throws {PerpsError} When the provider plugin is not registered, when
693
- * either the plugin read or the asset sync fails, or when a row's
814
+ * either the plugin read or the asset sync fails, when a row's
694
815
  * `withdrawalFee` is not a non-negative decimal.
695
816
  * @public
696
817
  */
@@ -742,6 +863,9 @@ export class PerpsClient {
742
863
  route: row.route,
743
864
  available: row.available,
744
865
  withdrawalFee: row.withdrawalFee,
866
+ ...(row.isFeeDeducted === undefined
867
+ ? {}
868
+ : { isFeeDeducted: row.isFeeDeducted }),
745
869
  },
746
870
  ]
747
871
  })
@@ -773,15 +897,17 @@ export class PerpsClient {
773
897
  }
774
898
 
775
899
  /**
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.
900
+ * Return the setup steps this account must still sign as a flat,
901
+ * self-describing list. Trading is gated on `isReady === true`.
902
+ * `checklist` carries the renderable onboarding list: every choice step and
903
+ * every `USER`-signed step with its satisfied state, with not-required
904
+ * conditional steps omitted.
781
905
  *
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`.
906
+ * A non-choice `SDK`-signed step is NEVER returned here — the SDK drains it
907
+ * with the provider's own credentials. A choice is listed but never staged
908
+ * or executed, whatever its `signer`: the user picks an option through
909
+ * {@link executeProviderOption}, and `isReady` stays `false` until every
910
+ * choice on `checklist` is satisfied.
785
911
  *
786
912
  * @public
787
913
  */
@@ -807,19 +933,15 @@ export class PerpsClient {
807
933
  }
808
934
  }
809
935
 
810
- const satisfiedSetup = await this.resolveSatisfiedSetup(provider, address)
936
+ const settings = await this.resolveSetupSettings(provider, address)
937
+ const satisfiedSetup = satisfiedTypes(settings)
811
938
  const pendingSetup = metadata.setup.filter(
812
939
  (descriptor) => !satisfiedSetup.has(descriptor.type)
813
940
  )
814
941
 
815
942
  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
- )
943
+
944
+ const stageable = pendingSetup.filter(isStageable)
823
945
 
824
946
  // The backend filters already-satisfied setup actions and returns typed
825
947
  // data for those still outstanding; each plugin contributes its own
@@ -827,7 +949,7 @@ export class PerpsClient {
827
949
  const actions = await this.buildProviderSetupActions(
828
950
  provider,
829
951
  address,
830
- visibleSetup
952
+ stageable
831
953
  )
832
954
 
833
955
  // A staged step is one the build produced actions for — a pending
@@ -835,69 +957,64 @@ export class PerpsClient {
835
957
  // (backend-gated) or not applicable to this account.
836
958
  const stagedTypes = new Set(actions.map((step) => step.action))
837
959
  const conditionalTypes = new Set(plugin?.conditionalSetupActions ?? [])
838
- const sequenceOf = (descriptor: ProviderAction): number =>
839
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
840
960
  const checklist = metadata.setup
841
- .filter((descriptor) => descriptor.signers.includes(PerpsSigner.USER))
961
+ .filter(isUserFacingSetupStep)
842
962
  .filter(
843
963
  (descriptor) =>
964
+ descriptor.options !== null ||
844
965
  !conditionalTypes.has(descriptor.type) ||
845
966
  stagedTypes.has(descriptor.type)
846
967
  )
847
968
  .sort((a, b) => sequenceOf(a) - sequenceOf(b))
848
969
  .map((descriptor) => ({
849
970
  descriptor,
850
- satisfied: !stagedTypes.has(descriptor.type),
971
+ satisfied:
972
+ descriptor.options !== null
973
+ ? satisfiedSetup.has(descriptor.type)
974
+ : !stagedTypes.has(descriptor.type),
975
+ selected: selectedOption(descriptor, settings),
976
+ currentValue: currentChoiceValue(descriptor, settings),
851
977
  }))
852
978
 
853
- await this.drainInternalSetup(
979
+ await this.drainSetup(
854
980
  provider,
855
981
  address,
856
- pendingSetup.filter(isInternal),
857
- visibleSetup.filter((descriptor) => stagedTypes.has(descriptor.type))
982
+ pendingSetup.filter(
983
+ (descriptor) =>
984
+ descriptor.options === null && descriptor.signer === PerpsSigner.SDK
985
+ ),
986
+ stageable.filter((descriptor) => stagedTypes.has(descriptor.type))
858
987
  )
859
988
 
860
989
  return {
861
990
  accountExists: true,
862
991
  setup: actions,
863
- isReady: actions.length === 0,
992
+ isReady:
993
+ actions.length === 0 && checklist.every((item) => item.satisfied),
864
994
  checklist,
865
995
  }
866
996
  }
867
997
 
868
998
  /**
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.
999
+ * Project the provider's own typed account config onto its setup steps. This
1000
+ * catches client-held auth state (e.g. Ondo SIWE/JWT) that the backend
1001
+ * cannot observe, and carries the current value of each choice.
872
1002
  */
873
- private async resolveSatisfiedSetup(
1003
+ private async resolveSetupSettings(
874
1004
  provider: string,
875
1005
  address: Address
876
- ): Promise<Set<ActionType>> {
1006
+ ): Promise<AccountConfigSetting[]> {
877
1007
  const metadata = await this.getProviderMetadata(provider)
878
1008
  const plugin = this.sdkClient.getProvider(provider)
879
1009
  if (!plugin || typeof plugin.getAccount !== 'function') {
880
- return new Set()
1010
+ return []
881
1011
  }
882
1012
  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
- )
1013
+ return plugin.projectConfig(account.config, metadata.setup)
896
1014
  }
897
1015
 
898
1016
  /**
899
- * Drain the pending setup steps the provider declares as internal via
900
- * `PerpsProviderPlugin.internalSetupActions`. Each step is built, signed,
1017
+ * Drain the pending `SDK`-signed non-choice steps: each is built, signed,
901
1018
  * and executed in place with the provider's own credentials. A step is
902
1019
  * deferred while any staged user-facing step with a lower `sequence` is
903
1020
  * outstanding — it cannot succeed before its prerequisite (e.g. SET_REFERRAL
@@ -906,17 +1023,15 @@ export class PerpsClient {
906
1023
  * blocks setup — the step stays unsatisfied and is retried on a later
907
1024
  * `checkSetup`.
908
1025
  */
909
- private async drainInternalSetup(
1026
+ private async drainSetup(
910
1027
  provider: string,
911
1028
  address: Address,
912
- internalPending: ProviderAction[],
913
- stagedVisible: ProviderAction[]
1029
+ drainable: SetupAction[],
1030
+ stagedVisible: SetupAction[]
914
1031
  ): Promise<void> {
915
- const sequenceOf = (descriptor: ProviderAction): number =>
916
- descriptor.sequence ?? Number.MAX_SAFE_INTEGER
917
- for (const descriptor of internalPending) {
1032
+ for (const descriptor of drainable) {
918
1033
  // A tie (equal sequences, or both absent) declares no order, so the
919
- // internal step defers to the next checkSetup rather than racing.
1034
+ // drained step defers to the next checkSetup rather than racing.
920
1035
  const blocked = stagedVisible.some(
921
1036
  (staged) => sequenceOf(staged) <= sequenceOf(descriptor)
922
1037
  )
@@ -944,7 +1059,7 @@ export class PerpsClient {
944
1059
  }
945
1060
  } catch (error) {
946
1061
  console.debug(
947
- `[perps-sdk] internal setup step '${descriptor.type}' for '${provider}' did not complete; will retry on the next checkSetup.`,
1062
+ `[perps-sdk] setup step '${descriptor.type}' for '${provider}' did not drain; will retry on the next checkSetup.`,
948
1063
  error
949
1064
  )
950
1065
  }
@@ -958,7 +1073,7 @@ export class PerpsClient {
958
1073
  private async buildProviderSetupActions(
959
1074
  provider: string,
960
1075
  address: Address,
961
- descriptors: ProviderAction[]
1076
+ descriptors: SetupAction[]
962
1077
  ): Promise<ActionStep[]> {
963
1078
  const setupPriority = (descriptor: ProviderAction): number =>
964
1079
  descriptor.signingMethod === SigningMethod.SIWE ? 0 : 1
@@ -996,9 +1111,11 @@ export class PerpsClient {
996
1111
 
997
1112
  /**
998
1113
  * 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).
1114
+ * ordered by descriptor `sequence`. Only a `USER`-signed non-choice step is
1115
+ * staged: an `SDK`-signed non-choice step drains inside {@link checkSetup}
1116
+ * and a choice executes the option the user picks. The backend filters
1117
+ * already-satisfied setup; each plugin contributes its own signer-bearing
1118
+ * request fields and any local-state params (e.g. Lighter's known pubkey).
1002
1119
  *
1003
1120
  * @public
1004
1121
  */
@@ -1011,7 +1128,7 @@ export class PerpsClient {
1011
1128
  const actions = await this.buildProviderSetupActions(
1012
1129
  provider,
1013
1130
  address,
1014
- metadata.setup
1131
+ metadata.setup.filter(isStageable)
1015
1132
  )
1016
1133
  return { actions }
1017
1134
  }
@@ -1054,13 +1171,7 @@ export class PerpsClient {
1054
1171
 
1055
1172
  await this.notifyExecuteResults(provider, address, results)
1056
1173
 
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
- }
1174
+ assertAllSucceeded(results)
1064
1175
 
1065
1176
  return { results: { results } }
1066
1177
  }
@@ -1068,25 +1179,23 @@ export class PerpsClient {
1068
1179
  /**
1069
1180
  * Sign and submit one pre-staged setup `ActionStep` end-to-end.
1070
1181
  *
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.
1182
+ * `step` and `checklist` must come from the same {@link checkSetup} result.
1183
+ * This preserves the nonce allocated while staging and validates sequence
1184
+ * prerequisites against that same snapshot, without another `createAction`
1185
+ * round-trip.
1079
1186
  *
1080
1187
  * @throws {PerpsError} When the step's action is not in the provider's
1081
- * `setup` descriptors.
1188
+ * `setup` descriptors, or when a lower-`sequence` step on the same
1189
+ * checklist, a choice included, is still unsatisfied.
1082
1190
  * @public
1083
1191
  */
1084
1192
  async executeProviderSetupAction(params: {
1085
1193
  provider: string
1086
1194
  address: Address
1087
1195
  step: ActionStep
1196
+ checklist: SetupChecklistItem[]
1088
1197
  }): Promise<void> {
1089
- const { provider, address, step } = params
1198
+ const { provider, address, step, checklist } = params
1090
1199
 
1091
1200
  const metadata = await this.getProviderMetadata(provider)
1092
1201
  const descriptor = metadata.setup.find((d) => d.type === step.action)
@@ -1097,6 +1206,17 @@ export class PerpsClient {
1097
1206
  )
1098
1207
  }
1099
1208
 
1209
+ const blockingStep = checklist.find(
1210
+ (item) =>
1211
+ !item.satisfied && sequenceOf(item.descriptor) < sequenceOf(descriptor)
1212
+ )
1213
+ if (blockingStep) {
1214
+ throw new PerpsError(
1215
+ PerpsErrorCode.SDKError,
1216
+ `Setup step '${step.action}' for '${provider}' is blocked: '${blockingStep.descriptor.type}' runs first and is not satisfied.`
1217
+ )
1218
+ }
1219
+
1100
1220
  const signed = await this.signProviderSetupAction(provider, address, step)
1101
1221
 
1102
1222
  // A client-executed setup action (Lighter SET_REFERRAL) produces no
@@ -1115,22 +1235,13 @@ export class PerpsClient {
1115
1235
  }
1116
1236
 
1117
1237
  /**
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.
1238
+ * Execute one option of a choice setup step (Lighter `ACCOUNT_TYPE`,
1239
+ * Hyperliquid `ACCOUNT_MODE`) end-to-end with its bound `{ type, params }`,
1240
+ * throwing on a venue rejection.
1121
1241
  *
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:
1242
+ * The option runs through the same {@link execute} pipeline as a trade, but
1124
1243
  * 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.
1244
+ * selection was rejected and must surface, not be silently dropped.
1134
1245
  *
1135
1246
  * @throws {PerpsError} Carrying the venue error when any returned result has
1136
1247
  * `success: false`, under that result's `errorCode` when the backend
@@ -1139,20 +1250,56 @@ export class PerpsClient {
1139
1250
  * signer, signing failure).
1140
1251
  * @public
1141
1252
  */
1142
- async executeProviderOption<T extends ActionType>(params: {
1253
+ async executeProviderOption(params: {
1143
1254
  provider: string
1144
1255
  address: Address
1145
- action: T
1146
- params: ActionParamsMap[T]
1256
+ option: SetupOption
1147
1257
  }): Promise<void> {
1148
- const { results } = await this.execute(params)
1149
- const failure = results.find((r) => !r.success)
1150
- if (failure) {
1258
+ const { provider, address, option } = params
1259
+ const { results } = await this.execute({
1260
+ provider,
1261
+ address,
1262
+ action: option.type,
1263
+ params: option.params,
1264
+ })
1265
+ assertAllSucceeded(results)
1266
+ }
1267
+
1268
+ /**
1269
+ * Undo a satisfied setup step by executing its `revoke` action through
1270
+ * create, sign and execute, signed with that action's `Provider.actions`
1271
+ * descriptor. The step stages again on the next {@link checkSetup}.
1272
+ *
1273
+ * @throws {PerpsError} `SDKError` when `step.revoke` is `null` or when
1274
+ * `createAction` stages nothing to revoke; otherwise the errors
1275
+ * {@link executeProviderOption} throws.
1276
+ * @public
1277
+ */
1278
+ async executeProviderRevoke(params: {
1279
+ provider: string
1280
+ address: Address
1281
+ step: SetupAction
1282
+ }): Promise<void> {
1283
+ const { provider, address, step } = params
1284
+ if (step.revoke === null) {
1151
1285
  throw new PerpsError(
1152
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1153
- failure.error
1286
+ PerpsErrorCode.SDKError,
1287
+ `Setup step '${step.type}' for '${provider}' declares no revoke.`
1154
1288
  )
1155
1289
  }
1290
+ const { staged, results } = await this.runAction({
1291
+ provider,
1292
+ address,
1293
+ action: step.revoke,
1294
+ params: {},
1295
+ })
1296
+ if (staged.length === 0) {
1297
+ throw new PerpsError(
1298
+ PerpsErrorCode.SDKError,
1299
+ `Nothing to revoke for setup step '${step.type}' on '${provider}'.`
1300
+ )
1301
+ }
1302
+ assertAllSucceeded(results)
1156
1303
  }
1157
1304
 
1158
1305
  /**
@@ -1333,6 +1480,18 @@ export class PerpsClient {
1333
1480
  * deposit); called as each leg is submitted and confirmed. */
1334
1481
  onProgress?: (progress: SignActionProgress) => void
1335
1482
  }): Promise<ExecuteActionResponse> {
1483
+ const { results } = await this.runAction(params)
1484
+ return { results }
1485
+ }
1486
+
1487
+ /** {@link execute}, also returning the steps `createAction` staged. */
1488
+ private async runAction<T extends ActionType>(params: {
1489
+ provider: string
1490
+ address: Address
1491
+ action: T
1492
+ params: ActionParamsMap[T]
1493
+ onProgress?: (progress: SignActionProgress) => void
1494
+ }): Promise<{ staged: ActionStep[]; results: ActionResult[] }> {
1336
1495
  const { provider, address, action, onProgress } = params
1337
1496
  const metadata = await this.getProviderMetadata(provider)
1338
1497
  const descriptor = findActionDescriptor(metadata, action)
@@ -1363,7 +1522,7 @@ export class PerpsClient {
1363
1522
  // token-authenticated venue mutations), leaving no backend-bound step. With
1364
1523
  // nothing to submit, skip the `/executeAction` hop.
1365
1524
  if (signedActions.length === 0) {
1366
- return { results: [] }
1525
+ return { staged: actions, results: [] }
1367
1526
  }
1368
1527
 
1369
1528
  const response = await executeAction(this.sdkClient, {
@@ -1380,7 +1539,7 @@ export class PerpsClient {
1380
1539
 
1381
1540
  await this.notifyExecuteResults(provider, address, results)
1382
1541
 
1383
- return { results }
1542
+ return { staged: actions, results }
1384
1543
  }
1385
1544
 
1386
1545
  /**