@lifi/perps-sdk 25.0.2 → 26.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 (88) hide show
  1. package/README.md +54 -1
  2. package/dist/cjs/client/PerpsClient.d.ts +5 -5
  3. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  4. package/dist/cjs/client/PerpsClient.js +75 -74
  5. package/dist/cjs/client/PerpsClient.js.map +1 -1
  6. package/dist/cjs/errors/PerpsExecutionError.d.ts +9 -0
  7. package/dist/cjs/errors/PerpsExecutionError.d.ts.map +1 -0
  8. package/dist/cjs/errors/PerpsExecutionError.js +29 -0
  9. package/dist/cjs/errors/PerpsExecutionError.js.map +1 -0
  10. package/dist/cjs/index.d.ts +5 -4
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +14 -12
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/math/fills.d.ts +0 -1
  15. package/dist/cjs/math/fills.d.ts.map +1 -1
  16. package/dist/cjs/math/fills.js +0 -18
  17. package/dist/cjs/math/fills.js.map +1 -1
  18. package/dist/cjs/math/order.d.ts +5 -5
  19. package/dist/cjs/math/order.d.ts.map +1 -1
  20. package/dist/cjs/math/order.js +10 -10
  21. package/dist/cjs/math/order.js.map +1 -1
  22. package/dist/cjs/math/position.d.ts +2 -2
  23. package/dist/cjs/math/position.d.ts.map +1 -1
  24. package/dist/cjs/math/position.js +5 -5
  25. package/dist/cjs/math/position.js.map +1 -1
  26. package/dist/cjs/types/api.d.ts +1 -10
  27. package/dist/cjs/types/api.d.ts.map +1 -1
  28. package/dist/cjs/types/provider.d.ts +5 -1
  29. package/dist/cjs/types/provider.d.ts.map +1 -1
  30. package/dist/cjs/version.d.ts +1 -1
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/esm/client/PerpsClient.d.ts +63 -47
  33. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  34. package/dist/esm/client/PerpsClient.js +136 -128
  35. package/dist/esm/client/PerpsClient.js.map +1 -1
  36. package/dist/esm/errors/PerpsExecutionError.d.ts +24 -0
  37. package/dist/esm/errors/PerpsExecutionError.d.ts.map +1 -0
  38. package/dist/esm/errors/PerpsExecutionError.js +39 -0
  39. package/dist/esm/errors/PerpsExecutionError.js.map +1 -0
  40. package/dist/esm/index.d.ts +5 -4
  41. package/dist/esm/index.d.ts.map +1 -1
  42. package/dist/esm/index.js +4 -3
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/math/fills.d.ts +0 -9
  45. package/dist/esm/math/fills.d.ts.map +1 -1
  46. package/dist/esm/math/fills.js +0 -25
  47. package/dist/esm/math/fills.js.map +1 -1
  48. package/dist/esm/math/order.d.ts +6 -6
  49. package/dist/esm/math/order.d.ts.map +1 -1
  50. package/dist/esm/math/order.js +6 -6
  51. package/dist/esm/math/order.js.map +1 -1
  52. package/dist/esm/math/position.d.ts +2 -2
  53. package/dist/esm/math/position.d.ts.map +1 -1
  54. package/dist/esm/math/position.js +3 -3
  55. package/dist/esm/math/position.js.map +1 -1
  56. package/dist/esm/types/api.d.ts +1 -18
  57. package/dist/esm/types/api.d.ts.map +1 -1
  58. package/dist/esm/types/provider.d.ts +21 -4
  59. package/dist/esm/types/provider.d.ts.map +1 -1
  60. package/dist/esm/version.d.ts +1 -1
  61. package/dist/esm/version.js +1 -1
  62. package/dist/types/client/PerpsClient.d.ts +63 -47
  63. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  64. package/dist/types/errors/PerpsExecutionError.d.ts +24 -0
  65. package/dist/types/errors/PerpsExecutionError.d.ts.map +1 -0
  66. package/dist/types/index.d.ts +5 -4
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/dist/types/math/fills.d.ts +0 -9
  69. package/dist/types/math/fills.d.ts.map +1 -1
  70. package/dist/types/math/order.d.ts +6 -6
  71. package/dist/types/math/order.d.ts.map +1 -1
  72. package/dist/types/math/position.d.ts +2 -2
  73. package/dist/types/math/position.d.ts.map +1 -1
  74. package/dist/types/types/api.d.ts +1 -18
  75. package/dist/types/types/api.d.ts.map +1 -1
  76. package/dist/types/types/provider.d.ts +21 -4
  77. package/dist/types/types/provider.d.ts.map +1 -1
  78. package/dist/types/version.d.ts +1 -1
  79. package/package.json +2 -2
  80. package/src/client/PerpsClient.ts +167 -164
  81. package/src/errors/PerpsExecutionError.ts +54 -0
  82. package/src/index.ts +12 -8
  83. package/src/math/fills.ts +0 -29
  84. package/src/math/order.ts +11 -11
  85. package/src/math/position.ts +3 -3
  86. package/src/types/api.ts +0 -21
  87. package/src/types/provider.ts +22 -4
  88. package/src/version.ts +1 -1
@@ -30,11 +30,13 @@ import {
30
30
  PerpsErrorCode,
31
31
  PerpsSigner,
32
32
  SigningMethod,
33
+ UserActionStatus,
33
34
  } from '@lifi/perps-types'
34
35
  import type { Address } from 'viem'
35
36
  import { isDecimalStringGreaterThan } from '../decimal/compare.js'
36
37
  import { unknownToDecimalString } from '../decimal/parse.js'
37
38
  import { PerpsError } from '../errors/PerpsError.js'
39
+ import { rethrowWithKnownResults } from '../errors/PerpsExecutionError.js'
38
40
  import { getAssetRegistry, toAssetDisplay } from '../registry/assetRegistry.js'
39
41
  import { getMarketRegistry } from '../registry/marketRegistry.js'
40
42
  import { createAction } from '../services/createAction.js'
@@ -55,8 +57,6 @@ import type {
55
57
  CancelTwapOrderParams,
56
58
  CreateReferralCodeActionParams,
57
59
  ExecuteMetaActionParams,
58
- ExecuteProviderSetupParams,
59
- ExecuteProviderSetupResult,
60
60
  GetAccountResult,
61
61
  GetDepositFlowParams,
62
62
  GetPortfolioHistoryParams,
@@ -87,6 +87,7 @@ import type {
87
87
  PerpsSDKClient,
88
88
  SignActionProgress,
89
89
  SignActionsContext,
90
+ SignActionsResult,
90
91
  } from '../types/provider.js'
91
92
  import type {
92
93
  WithdrawableBalance,
@@ -213,15 +214,11 @@ function currentChoiceValue(
213
214
  return value === null ? null : String(value)
214
215
  }
215
216
 
216
- /** Throw the first per-action failure a 200 OK `/executeAction` carried. */
217
- function assertAllSucceeded(results: ActionResult[]): void {
218
- const failure = results.find((r) => !r.success)
219
- if (failure) {
220
- throw new PerpsError(
221
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
222
- failure.error
223
- )
224
- }
217
+ function isAcknowledged(result: ActionResult): boolean {
218
+ return (
219
+ result.status === UserActionStatus.Submitted ||
220
+ result.status === UserActionStatus.Accepted
221
+ )
225
222
  }
226
223
 
227
224
  function isNonNegativeDecimal(value: string): boolean {
@@ -451,10 +448,13 @@ export class PerpsClient {
451
448
  * step with the signer its own descriptor declares; the signed steps come
452
449
  * back in the batch's original order. The plugin owns every signing arm and
453
450
  * branches on `signer` internally, reading the end-user's wallet from the
454
- * {@link SignActionsContext} when an arm signs as the user.
451
+ * {@link SignActionsContext} when an arm signs as the user. Signing stops
452
+ * after the first group that executed a step without an acknowledgement.
455
453
  *
456
454
  * @throws {PerpsError} When a step's action is not declared by the provider,
457
455
  * or the plugin implements no `signActions`.
456
+ * @throws {PerpsExecutionError} When a group throws after an earlier group
457
+ * executed steps.
458
458
  */
459
459
  private async delegateSignActions(
460
460
  provider: string,
@@ -462,36 +462,90 @@ export class PerpsClient {
462
462
  metadata: Provider,
463
463
  actions: ActionStep[],
464
464
  onProgress?: (progress: SignActionProgress) => void
465
- ): Promise<SignedActionStep[]> {
465
+ ): Promise<SignActionsResult> {
466
466
  const plugin = this.requireProvider(provider)
467
- if (typeof plugin.signActions !== 'function') {
467
+ const signActions = plugin.signActions?.bind(plugin)
468
+ if (signActions === undefined) {
468
469
  throw new PerpsError(
469
470
  PerpsErrorCode.SDKError,
470
471
  `Provider '${provider}' does not implement signActions.`
471
472
  )
472
473
  }
473
474
  const signed: SignedActionStep[] = []
475
+ const executed: ActionResult[] = []
474
476
  for (const group of groupStepsByDescriptor(metadata, actions)) {
475
- const userWallet = await this.resolveSigningWallet(
476
- group.descriptor,
477
- group.steps
478
- )
479
- signed.push(
480
- ...(await plugin.signActions(
477
+ try {
478
+ const userWallet = await this.resolveSigningWallet(
479
+ group.descriptor,
480
+ group.steps
481
+ )
482
+ const result = await signActions(
481
483
  group.descriptor.signingMethod,
482
484
  group.steps,
483
485
  address,
484
486
  this.buildSignActionsContext(group.descriptor, userWallet, onProgress)
485
- ))
486
- )
487
+ )
488
+ signed.push(...result.signed)
489
+ executed.push(...result.executed)
490
+ } catch (error) {
491
+ rethrowWithKnownResults(executed, error)
492
+ }
493
+ if (!executed.every(isAcknowledged)) {
494
+ break
495
+ }
487
496
  }
488
- return signed
497
+ return { signed, executed }
489
498
  }
490
499
 
491
500
  /**
492
- * Let the plugin observe `/executeAction` per-step results before the core
493
- * surfaces failures — e.g. to evict a locally stored credential the venue
494
- * rejected.
501
+ * Submit `signed` to `/executeAction` when every step the plugin executed
502
+ * while signing was acknowledged, then resolve explorer links and notify the
503
+ * plugin. Resolves to the full report: `executed`, then the `/executeAction`
504
+ * results. An empty report means nothing executed.
505
+ *
506
+ * @throws {PerpsExecutionError} When the submission or the plugin callback
507
+ * throws after a result is known.
508
+ */
509
+ private async submitSignedActions(
510
+ provider: string,
511
+ address: Address,
512
+ action: ActionType,
513
+ signerAddress: Address | undefined,
514
+ { signed, executed }: SignActionsResult
515
+ ): Promise<ActionResult[]> {
516
+ let results = executed
517
+ if (signed.length > 0 && executed.every(isAcknowledged)) {
518
+ try {
519
+ const response = await executeAction(this.sdkClient, {
520
+ provider,
521
+ address,
522
+ // The submitting account: the plugin-resolved signer (Hyperliquid's
523
+ // agent) when present, else the end-user's address.
524
+ signerAddress: signerAddress ?? address,
525
+ action,
526
+ actions: signed,
527
+ })
528
+ results = [...executed, ...response.results]
529
+ } catch (error) {
530
+ rethrowWithKnownResults(executed, error)
531
+ }
532
+ }
533
+ if (results.length === 0) {
534
+ return results
535
+ }
536
+
537
+ try {
538
+ results = this.resolveExplorerLinks(provider, results)
539
+ await this.notifyExecuteResults(provider, address, results)
540
+ } catch (error) {
541
+ rethrowWithKnownResults(results, error)
542
+ }
543
+ return results
544
+ }
545
+
546
+ /**
547
+ * Let the plugin observe an execution report before the core returns it —
548
+ * e.g. to evict a locally stored credential the venue refused.
495
549
  */
496
550
  private async notifyExecuteResults(
497
551
  provider: string,
@@ -505,9 +559,10 @@ export class PerpsClient {
505
559
  }
506
560
 
507
561
  /**
508
- * Attach a provider-built explorer URL to every result the backend returned a
509
- * venue `txHash` on. The provider owns its explorer target, so core only asks
510
- * — a plugin without the hook leaves results untouched.
562
+ * Attach a provider-built explorer URL to every result that carries a venue
563
+ * `txHash`, whatever its status: an unacknowledged hash is what the user
564
+ * inspects. The provider owns its explorer target, so core only asks — a
565
+ * plugin without the hook leaves results untouched.
511
566
  */
512
567
  private resolveExplorerLinks(
513
568
  provider: string,
@@ -519,7 +574,7 @@ export class PerpsClient {
519
574
  return results
520
575
  }
521
576
  return results.map((result) => {
522
- if (!result.success || result.txHash === undefined) {
577
+ if (result.txHash === undefined) {
523
578
  return result
524
579
  }
525
580
  const explorerLink = resolveLink(result.txHash)
@@ -610,30 +665,47 @@ export class PerpsClient {
610
665
  }
611
666
 
612
667
  /**
613
- * Sign a single provider setup action step by delegating to the provider
614
- * plugin, which branches on the step's signing scheme internally. Lets
615
- * consumers collect signed setup actions without embedding per-method
616
- * signing logic.
617
- *
618
- * Returns `undefined` when the plugin executed the action entirely
619
- * client-side (e.g. Lighter's token-authenticated venue mutations), leaving
620
- * no backend-bound step to submit.
668
+ * Sign one provider setup step through the provider plugin and submit it.
669
+ * A step the plugin executed client-side (e.g. Lighter's token-authenticated
670
+ * venue mutations) reports the plugin's own result instead of an
671
+ * `/executeAction` result.
621
672
  *
622
- * @throws {PerpsError} When the step's action is not declared by the provider.
673
+ * @throws {PerpsError} When the step's action is not declared by the provider,
674
+ * or signing or submission fails before any result is known.
675
+ * @throws {PerpsExecutionError} When a later stage fails after a result is
676
+ * known.
623
677
  */
624
- private async signProviderSetupAction(
678
+ private async runProviderSetupStep(
625
679
  provider: string,
626
680
  address: Address,
627
681
  step: ActionStep
628
- ): Promise<SignedActionStep | undefined> {
682
+ ): Promise<ActionResult[]> {
629
683
  const metadata = await this.getProviderMetadata(provider)
630
- const [signed] = await this.delegateSignActions(
684
+ const descriptor = findActionDescriptor(metadata, step.action)
685
+ const signResult = await this.delegateSignActions(
631
686
  provider,
632
687
  address,
633
688
  metadata,
634
689
  [step]
635
690
  )
636
- return signed
691
+ let signerAddress: Address | undefined
692
+ try {
693
+ const request = await this.resolveActionRequest(
694
+ provider,
695
+ descriptor,
696
+ address
697
+ )
698
+ signerAddress = request.signerAddress
699
+ } catch (error) {
700
+ rethrowWithKnownResults(signResult.executed, error)
701
+ }
702
+ return this.submitSignedActions(
703
+ provider,
704
+ address,
705
+ step.action,
706
+ signerAddress,
707
+ signResult
708
+ )
637
709
  }
638
710
 
639
711
  /**
@@ -1142,9 +1214,9 @@ export class PerpsClient {
1142
1214
  * deferred while any staged user-facing step with a lower `sequence` is
1143
1215
  * outstanding — it cannot succeed before its prerequisite (e.g. SET_REFERRAL
1144
1216
  * authenticates with the credential REGISTER_API_KEY installs) and each
1145
- * doomed attempt is venue traffic. A drain failure is swallowed so it never
1146
- * blocks setup — the step stays unsatisfied and is retried on a later
1147
- * `checkSetup`.
1217
+ * doomed attempt is venue traffic. A drain failure, thrown or reported, is
1218
+ * swallowed so it never blocks setup — the step stays unsatisfied and is
1219
+ * retried on a later `checkSetup`.
1148
1220
  */
1149
1221
  private async drainSetup(
1150
1222
  provider: string,
@@ -1166,18 +1238,18 @@ export class PerpsClient {
1166
1238
  descriptor,
1167
1239
  ])
1168
1240
  for (const step of steps) {
1169
- const signed = await this.signProviderSetupAction(
1241
+ const results = await this.runProviderSetupStep(
1170
1242
  provider,
1171
1243
  address,
1172
1244
  step
1173
1245
  )
1174
- if (signed !== undefined) {
1175
- await this.executeProviderSetup({
1176
- provider,
1177
- address,
1178
- setup: [step],
1179
- signedActions: [signed],
1180
- })
1246
+ const failure = results.find((result) => !isAcknowledged(result))
1247
+ if (failure !== undefined) {
1248
+ console.debug(
1249
+ `[perps-sdk] setup step '${descriptor.type}' for '${provider}' reported '${failure.status}'; will retry on the next checkSetup.`,
1250
+ failure
1251
+ )
1252
+ break
1181
1253
  }
1182
1254
  }
1183
1255
  } catch (error) {
@@ -1256,49 +1328,6 @@ export class PerpsClient {
1256
1328
  return { actions }
1257
1329
  }
1258
1330
 
1259
- /**
1260
- * Submit the signed setup steps returned by `checkSetup` (and signed by the
1261
- * caller / `signProviderSetupAction`). Routes the batch on the first step's
1262
- * action and lets the plugin contribute that action's `signerAddress`.
1263
- * Throws on any per-step venue rejection.
1264
- */
1265
- private async executeProviderSetup(
1266
- params: ExecuteProviderSetupParams
1267
- ): Promise<ExecuteProviderSetupResult> {
1268
- const { provider, address, setup, signedActions } = params
1269
-
1270
- if (signedActions.length === 0) {
1271
- return { results: { results: [] } }
1272
- }
1273
-
1274
- const action = setup[0]?.action ?? signedActions[0].action
1275
- const metadata = await this.getProviderMetadata(provider)
1276
- const descriptor = findActionDescriptor(metadata, action)
1277
- const { signerAddress } = await this.resolveActionRequest(
1278
- provider,
1279
- descriptor,
1280
- address
1281
- )
1282
-
1283
- const response = await executeAction(this.sdkClient, {
1284
- provider,
1285
- address,
1286
- // The submitting account: the plugin-resolved signer (Hyperliquid's
1287
- // agent) when present, else the end-user's address.
1288
- signerAddress: signerAddress ?? address,
1289
- action,
1290
- actions: signedActions,
1291
- })
1292
-
1293
- const results = this.resolveExplorerLinks(provider, response.results)
1294
-
1295
- await this.notifyExecuteResults(provider, address, results)
1296
-
1297
- assertAllSucceeded(results)
1298
-
1299
- return { results: { results } }
1300
- }
1301
-
1302
1331
  /**
1303
1332
  * Sign and submit one pre-staged setup `ActionStep` end-to-end.
1304
1333
  *
@@ -1307,9 +1336,15 @@ export class PerpsClient {
1307
1336
  * prerequisites against that same snapshot, without another `createAction`
1308
1337
  * round-trip.
1309
1338
  *
1339
+ * Resolves to the step's execution report. A refused or indeterminate step
1340
+ * is a result in the report, not an exception.
1341
+ *
1310
1342
  * @throws {PerpsError} When the step's action is not in the provider's
1311
- * `setup` descriptors, or when a lower-`sequence` step on the same
1312
- * checklist, a choice included, is still unsatisfied.
1343
+ * `setup` descriptors, when a lower-`sequence` step on the same
1344
+ * checklist, a choice included, is still unsatisfied, or when signing or
1345
+ * submission fails before any result is known.
1346
+ * @throws {PerpsExecutionError} When a later stage fails after a result is
1347
+ * known; its `response` holds the known results.
1313
1348
  * @public
1314
1349
  */
1315
1350
  async executeProviderSetupAction(params: {
@@ -1317,7 +1352,7 @@ export class PerpsClient {
1317
1352
  address: Address
1318
1353
  step: ActionStep
1319
1354
  checklist: SetupChecklistItem[]
1320
- }): Promise<void> {
1355
+ }): Promise<ExecuteActionResponse> {
1321
1356
  const { provider, address, step, checklist } = params
1322
1357
 
1323
1358
  const metadata = await this.getProviderMetadata(provider)
@@ -1340,52 +1375,34 @@ export class PerpsClient {
1340
1375
  )
1341
1376
  }
1342
1377
 
1343
- const signed = await this.signProviderSetupAction(provider, address, step)
1344
-
1345
- // A client-executed setup action (Lighter SET_REFERRAL) produces no
1346
- // backend-bound step — it already ran during signing, so there is nothing
1347
- // to submit.
1348
- if (signed === undefined) {
1349
- return
1378
+ return {
1379
+ results: await this.runProviderSetupStep(provider, address, step),
1350
1380
  }
1351
-
1352
- await this.executeProviderSetup({
1353
- provider,
1354
- address,
1355
- setup: [step],
1356
- signedActions: [signed],
1357
- })
1358
1381
  }
1359
1382
 
1360
1383
  /**
1361
1384
  * Execute one option of a choice setup step (Lighter `ACCOUNT_TYPE`,
1362
1385
  * Hyperliquid `ACCOUNT_MODE`) end-to-end with its bound `{ type, params }`,
1363
- * throwing on a venue rejection.
1386
+ * through the same {@link execute} pipeline as a trade.
1364
1387
  *
1365
- * The option runs through the same {@link execute} pipeline as a trade, but
1366
- * a per-action `success: false` (returned as a 200 OK) means the user's
1367
- * selection was rejected and must surface, not be silently dropped.
1388
+ * Resolves to the execution report. Read each result's `status`: a refused
1389
+ * selection is a `Failed` result, not an exception.
1368
1390
  *
1369
- * @throws {PerpsError} Carrying the venue error when any returned result has
1370
- * `success: false`, under that result's `errorCode` when the backend
1371
- * classified the failure and `PerpsErrorCode.ExchangeRejected` otherwise;
1372
- * also the errors `execute` itself can throw (unregistered provider, no
1373
- * signer, signing failure).
1391
+ * @throws {PerpsError} The errors {@link execute} throws.
1374
1392
  * @public
1375
1393
  */
1376
1394
  async executeProviderOption(params: {
1377
1395
  provider: string
1378
1396
  address: Address
1379
1397
  option: SetupOption
1380
- }): Promise<void> {
1398
+ }): Promise<ExecuteActionResponse> {
1381
1399
  const { provider, address, option } = params
1382
- const { results } = await this.execute({
1400
+ return this.execute({
1383
1401
  provider,
1384
1402
  address,
1385
1403
  action: option.type,
1386
1404
  params: option.params,
1387
1405
  })
1388
- assertAllSucceeded(results)
1389
1406
  }
1390
1407
 
1391
1408
  /**
@@ -1393,16 +1410,18 @@ export class PerpsClient {
1393
1410
  * create, sign and execute, signed with that action's `Provider.actions`
1394
1411
  * descriptor. The step stages again on the next {@link checkSetup}.
1395
1412
  *
1413
+ * Resolves to the execution report.
1414
+ *
1396
1415
  * @throws {PerpsError} `SDKError` when `step.revoke` is `null` or when
1397
1416
  * `createAction` stages nothing to revoke; otherwise the errors
1398
- * {@link executeProviderOption} throws.
1417
+ * {@link execute} throws.
1399
1418
  * @public
1400
1419
  */
1401
1420
  async executeProviderRevoke(params: {
1402
1421
  provider: string
1403
1422
  address: Address
1404
1423
  step: SetupAction
1405
- }): Promise<void> {
1424
+ }): Promise<ExecuteActionResponse> {
1406
1425
  const { provider, address, step } = params
1407
1426
  if (step.revoke === null) {
1408
1427
  throw new PerpsError(
@@ -1422,7 +1441,7 @@ export class PerpsClient {
1422
1441
  `Nothing to revoke for setup step '${step.type}' on '${provider}'.`
1423
1442
  )
1424
1443
  }
1425
- assertAllSucceeded(results)
1444
+ return { results }
1426
1445
  }
1427
1446
 
1428
1447
  /**
@@ -1590,8 +1609,15 @@ export class PerpsClient {
1590
1609
  * descriptor's scheme and signer internally), and submit. Core stays
1591
1610
  * signer-agnostic — the plugin owns WHO signs and HOW.
1592
1611
  *
1612
+ * Resolves to the execution report: one result per executed step, in
1613
+ * execution order, each with its own `status`. An empty report means
1614
+ * nothing executed.
1615
+ *
1593
1616
  * @throws {PerpsError} When the action is not declared by the provider,
1594
- * the plugin cannot sign it, or submission fails.
1617
+ * the plugin cannot sign it, or submission fails before any result is
1618
+ * known.
1619
+ * @throws {PerpsExecutionError} When a later stage fails after a result is
1620
+ * known; its `response` holds the known results.
1595
1621
  * @public
1596
1622
  */
1597
1623
  async execute<T extends ActionType>(params: {
@@ -1633,7 +1659,7 @@ export class PerpsClient {
1633
1659
  } as ActionParamsMap[T],
1634
1660
  })
1635
1661
 
1636
- const signedActions = await this.delegateSignActions(
1662
+ const signResult = await this.delegateSignActions(
1637
1663
  provider,
1638
1664
  address,
1639
1665
  metadata,
@@ -1641,27 +1667,13 @@ export class PerpsClient {
1641
1667
  onProgress
1642
1668
  )
1643
1669
 
1644
- // A plugin may execute an action entirely client-side (e.g. Lighter's
1645
- // token-authenticated venue mutations), leaving no backend-bound step. With
1646
- // nothing to submit, skip the `/executeAction` hop.
1647
- if (signedActions.length === 0) {
1648
- return { staged: actions, results: [] }
1649
- }
1650
-
1651
- const response = await executeAction(this.sdkClient, {
1670
+ const results = await this.submitSignedActions(
1652
1671
  provider,
1653
1672
  address,
1654
- // The submitting account: the plugin-resolved signer (Hyperliquid's
1655
- // agent) when present, else the end-user's address.
1656
- signerAddress: signerAddress ?? address,
1657
1673
  action,
1658
- actions: signedActions,
1659
- })
1660
-
1661
- const results = this.resolveExplorerLinks(provider, response.results)
1662
-
1663
- await this.notifyExecuteResults(provider, address, results)
1664
-
1674
+ signerAddress,
1675
+ signResult
1676
+ )
1665
1677
  return { staged: actions, results }
1666
1678
  }
1667
1679
 
@@ -1717,12 +1729,13 @@ export class PerpsClient {
1717
1729
  * typed data with the configured user wallet.
1718
1730
  *
1719
1731
  * The backend returns at most one step. Resolves to `{ results: [] }` when it
1720
- * returns none, meaning no consent is outstanding for this address.
1732
+ * returns none, meaning no consent is outstanding for this address. Else it
1733
+ * resolves to the backend's execution report; read each result's `status`.
1721
1734
  *
1722
1735
  * @throws {PerpsError} When no user wallet is configured, the backend returns
1723
- * more than one step or a non-EIP-712 step, or the submitted step reports a
1724
- * failed result. A signature refusal from the wallet client propagates as
1725
- * the wallet's own error.
1736
+ * more than one step or a non-EIP-712 step, or the submission fails. A
1737
+ * signature refusal from the wallet client propagates as the wallet's own
1738
+ * error.
1726
1739
  * @public
1727
1740
  */
1728
1741
  async executeMetaAction<T extends MetaActionType>(
@@ -1757,21 +1770,11 @@ export class PerpsClient {
1757
1770
  const wallet = await this.resolveMetaSigningWallet(actions)
1758
1771
  const signature = await signTypedDataWithSigner(wallet, step.typedData)
1759
1772
 
1760
- const response = await executeAction(this.sdkClient, {
1773
+ return executeAction(this.sdkClient, {
1761
1774
  provider: META_PROVIDER,
1762
1775
  address,
1763
1776
  action,
1764
1777
  actions: [{ ...step, signature }],
1765
1778
  })
1766
-
1767
- const failure = response.results.find((r) => !r.success)
1768
- if (failure) {
1769
- throw new PerpsError(
1770
- failure.errorCode ?? PerpsErrorCode.ExchangeRejected,
1771
- failure.error
1772
- )
1773
- }
1774
-
1775
- return response
1776
1779
  }
1777
1780
  }
@@ -0,0 +1,54 @@
1
+ import {
2
+ type ActionResult,
3
+ type ExecuteActionResponse,
4
+ PerpsErrorCode,
5
+ } from '@lifi/perps-types'
6
+ import { PerpsError } from './PerpsError.js'
7
+
8
+ /**
9
+ * Error thrown when an execution stage fails after part of the execution
10
+ * report is known. `response` holds every result known at the failure, and
11
+ * `cause` holds the original error. Its `code` is the cause's code when the
12
+ * cause is a {@link PerpsError}, else `SDKError`.
13
+ *
14
+ * @public
15
+ */
16
+ export class PerpsExecutionError extends PerpsError {
17
+ override name = 'PerpsExecutionError'
18
+ readonly response: ExecuteActionResponse
19
+
20
+ constructor(response: ExecuteActionResponse, cause: unknown) {
21
+ super(
22
+ cause instanceof PerpsError ? cause.code : PerpsErrorCode.SDKError,
23
+ cause instanceof Error ? cause.message : String(cause)
24
+ )
25
+ this.response = response
26
+ this.cause = cause
27
+ if (cause instanceof PerpsError && cause.tool !== undefined) {
28
+ this.tool = cause.tool
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Rethrow `error` unchanged when `known` is empty. Otherwise throw a
35
+ * {@link PerpsExecutionError} whose report holds `known`, then any results
36
+ * `error` already carries.
37
+ *
38
+ * @public
39
+ */
40
+ export function rethrowWithKnownResults(
41
+ known: ActionResult[],
42
+ error: unknown
43
+ ): never {
44
+ if (known.length === 0) {
45
+ throw error
46
+ }
47
+ if (error instanceof PerpsExecutionError) {
48
+ throw new PerpsExecutionError(
49
+ { results: [...known, ...error.response.results] },
50
+ error.cause
51
+ )
52
+ }
53
+ throw new PerpsExecutionError({ results: known }, error)
54
+ }
package/src/index.ts CHANGED
@@ -71,35 +71,38 @@ export {
71
71
  export { PerpsErrorMessage } from './errors/constants.js'
72
72
  export { PerpsError } from './errors/PerpsError.js'
73
73
  export {
74
- classifyFill,
74
+ PerpsExecutionError,
75
+ rethrowWithKnownResults,
76
+ } from './errors/PerpsExecutionError.js'
77
+ export {
75
78
  classifyFillFromPosition,
76
79
  FillClassification,
77
80
  } from './math/fills.js'
78
81
  export type { ExpectedPnl } from './math/order.js'
79
82
  export {
80
- applySlippageToPrice,
81
83
  buildQuote,
82
84
  calculateExpectedPnl,
85
+ calculateOrderbookWalk,
83
86
  calculateRealizedPnlPercent,
84
87
  calculateSize,
88
+ calculateSlippagePrice,
85
89
  calculateTriggerPercent,
86
90
  calculateTriggerPrice,
87
91
  estimateFees,
88
92
  estimateRealizedPnl,
89
- findMatchingPosition,
90
93
  resolveCloseSize,
91
- safeApplySlippageToPrice,
94
+ resolveMatchingPosition,
92
95
  safeBuildQuote,
93
96
  safeCalculateExpectedPnl,
97
+ safeCalculateOrderbookWalk,
94
98
  safeCalculateRealizedPnlPercent,
95
99
  safeCalculateSize,
100
+ safeCalculateSlippagePrice,
96
101
  safeCalculateTriggerPercent,
97
102
  safeCalculateTriggerPrice,
98
103
  safeEstimateFees,
99
104
  safeEstimateRealizedPnl,
100
105
  safeResolveCloseSize,
101
- safeWalkOrderbook,
102
- walkOrderbook,
103
106
  } from './math/order.js'
104
107
  export {
105
108
  calculateEffectiveLeverage,
@@ -114,8 +117,8 @@ export {
114
117
  estimateLiquidationPriceAtMarketRate,
115
118
  estimateNewLeverage,
116
119
  estimateUnrealizedPnl,
117
- positionSupportsMarginAdjustment,
118
- positionSupportsMarginRemoval,
120
+ isPositionMarginAdjustable,
121
+ isPositionMarginRemovable,
119
122
  safeCalculateEffectiveLeverage,
120
123
  safeCalculateLiquidationDistance,
121
124
  safeCalculateNotionalValue,
@@ -283,6 +286,7 @@ export type {
283
286
  QuoteListener,
284
287
  SignActionProgress,
285
288
  SignActionsContext,
289
+ SignActionsResult,
286
290
  } from './types/provider.js'
287
291
  export type {
288
292
  ProviderWithdrawableBalance,