@metamask-previews/perps-controller 17.4.0-preview-fb486948f → 17.4.0-preview-aecbce4ff

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 (55) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/PerpsController-method-action-types.d.ts +10 -3
  3. package/dist/PerpsController-method-action-types.d.ts.map +1 -1
  4. package/dist/PerpsController-method-action-types.js.map +1 -1
  5. package/dist/PerpsController.d.ts +10 -3
  6. package/dist/PerpsController.d.ts.map +1 -1
  7. package/dist/PerpsController.js +62 -8
  8. package/dist/PerpsController.js.map +1 -1
  9. package/dist/constants/perpsConfig.d.ts +55 -0
  10. package/dist/constants/perpsConfig.d.ts.map +1 -1
  11. package/dist/constants/perpsConfig.js +55 -0
  12. package/dist/constants/perpsConfig.js.map +1 -1
  13. package/dist/providers/AggregatedPerpsProvider.d.ts +19 -0
  14. package/dist/providers/AggregatedPerpsProvider.d.ts.map +1 -1
  15. package/dist/providers/AggregatedPerpsProvider.js +21 -0
  16. package/dist/providers/AggregatedPerpsProvider.js.map +1 -1
  17. package/dist/providers/HyperLiquidProvider.d.ts +7 -0
  18. package/dist/providers/HyperLiquidProvider.d.ts.map +1 -1
  19. package/dist/providers/HyperLiquidProvider.js +133 -34
  20. package/dist/providers/HyperLiquidProvider.js.map +1 -1
  21. package/dist/providers/LighterProvider.d.ts.map +1 -1
  22. package/dist/providers/LighterProvider.js +2 -0
  23. package/dist/providers/LighterProvider.js.map +1 -1
  24. package/dist/services/MarketDataService.d.ts.map +1 -1
  25. package/dist/services/MarketDataService.js +16 -4
  26. package/dist/services/MarketDataService.js.map +1 -1
  27. package/dist/services/RewardsIntegrationService.d.ts +47 -2
  28. package/dist/services/RewardsIntegrationService.d.ts.map +1 -1
  29. package/dist/services/RewardsIntegrationService.js +400 -19
  30. package/dist/services/RewardsIntegrationService.js.map +1 -1
  31. package/dist/services/ServiceContext.d.ts +9 -1
  32. package/dist/services/ServiceContext.d.ts.map +1 -1
  33. package/dist/services/ServiceContext.js.map +1 -1
  34. package/dist/services/TradingService.d.ts.map +1 -1
  35. package/dist/services/TradingService.js +291 -18
  36. package/dist/services/TradingService.js.map +1 -1
  37. package/dist/types/index.d.ts +57 -3
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js.map +1 -1
  40. package/dist/types/messenger.d.ts +17 -1
  41. package/dist/types/messenger.d.ts.map +1 -1
  42. package/dist/types/messenger.js.map +1 -1
  43. package/dist/utils/hyperLiquidAdapter.d.ts +7 -0
  44. package/dist/utils/hyperLiquidAdapter.d.ts.map +1 -1
  45. package/dist/utils/hyperLiquidAdapter.js +1 -1
  46. package/dist/utils/hyperLiquidAdapter.js.map +1 -1
  47. package/dist/utils/index.d.ts +1 -0
  48. package/dist/utils/index.d.ts.map +1 -1
  49. package/dist/utils/index.js +1 -0
  50. package/dist/utils/index.js.map +1 -1
  51. package/dist/utils/subscriptionFeeWaiver.d.ts +201 -0
  52. package/dist/utils/subscriptionFeeWaiver.d.ts.map +1 -0
  53. package/dist/utils/subscriptionFeeWaiver.js +334 -0
  54. package/dist/utils/subscriptionFeeWaiver.js.map +1 -0
  55. package/package.json +6 -1
@@ -19,7 +19,7 @@ import { addSpotBalanceToAccountState, aggregateAccountStates, } from '../utils/
19
19
  import { isValidCapabilitySymbol } from '../utils/capabilitySymbols.js';
20
20
  import { ensureError, isHyperLiquidMultiSigRequiredError, isHyperLiquidUserNotFoundError, isKeyringLockedError, } from '../utils/errorUtils.js';
21
21
  import { shouldDeferUnifiedAccountSetup } from '../utils/hyperLiquidAbstraction.js';
22
- import { adaptAccountStateFromSDK, adaptHyperLiquidLedgerUpdateToUserHistoryItem, adaptMarketFromSDK, adaptOrderFromSDK, adaptPositionFromSDK, adaptPositionTriggerOrderFromSDK, adaptTpslLinkageToGrouping, buildAssetMapping, formatHyperLiquidPrice, formatHyperLiquidSize, HYPERLIQUID_SCALE_CLOID_MARKER, parseAssetName, } from '../utils/hyperLiquidAdapter.js';
22
+ import { adaptAccountStateFromSDK, adaptHyperLiquidLedgerUpdateToUserHistoryItem, adaptMarketFromSDK, adaptOrderFromSDK, adaptPositionFromSDK, adaptPositionTriggerOrderFromSDK, adaptTpslLinkageToGrouping, buildAssetMapping, formatHyperLiquidPrice, formatHyperLiquidSize, HYPERLIQUID_SCALE_CLOID_MARKER, parseAssetName, readScaleGroupId, } from '../utils/hyperLiquidAdapter.js';
23
23
  import { previewHyperLiquidIsolatedPositionModify, resolveHyperLiquidMarginTiers, } from '../utils/hyperLiquidPositionPreview.js';
24
24
  import { createErrorResult, getMaxOrderValue, getSupportedPaths, validateAssetSupport, validateBalance, validateCoinExists, validateDepositParams, validateOrderParams, validateWithdrawalParams, } from '../utils/hyperLiquidValidation.js';
25
25
  import { generatePerpsId } from '../utils/idUtils.js';
@@ -29,6 +29,7 @@ import { buildOrdersArray, calculateFinalPositionSize, calculateOrderPriceAndSiz
29
29
  import { getTriggerDirection, getTriggerExecution, isLimitExecutionOrderType, isStrategyOrderType, isTriggerOrderType, resolvePositionTriggerSummaryPrice, toSDKTimeInForce, } from '../utils/orderTypes.js';
30
30
  import { createStandaloneInfoClient, queryStandaloneClearinghouseStates, queryStandaloneOpenOrders, } from '../utils/standaloneInfoClient.js';
31
31
  import { parseBoundedNonNegativeDecimal } from '../utils/stringParseUtils.js';
32
+ import { markSubscriptionCloid, quantizeBuilderFeeTenthsBps, } from '../utils/subscriptionFeeWaiver.js';
32
33
  // getStreamManagerInstance removed: use this.#deps.streamManager instead
33
34
  const HISTORICAL_ORDER_TYPE_BY_DETAILED_TYPE = {
34
35
  [DETAILED_ORDER_TYPES.LIMIT]: 'limit',
@@ -348,13 +349,18 @@ const validateStrategyNotional = (params) => {
348
349
  * rung. HyperLiquid requires every CLOID to be unique, so the last byte holds
349
350
  * the rung index while the preceding 15 bytes identify the shared group.
350
351
  *
352
+ * The byte right after the Scale marker is reserved for the subscription flag
353
+ * byte and is always zeroed here. Random entropy in that position would set the
354
+ * `fee_reduction_applied` bit roughly half the time, so an unmarked ladder would
355
+ * decode downstream as a waived one.
356
+ *
351
357
  * @param count - Number of ladder rungs.
352
358
  * @returns The public group handle and venue client order IDs.
353
359
  */
354
360
  const createScaleOrderIdentity = (count) => {
355
- const groupKey = `${HYPERLIQUID_SCALE_CLOID_MARKER}${uuidv4()
361
+ const groupKey = `${HYPERLIQUID_SCALE_CLOID_MARKER}00${uuidv4()
356
362
  .replace(/-/gu, '')
357
- .slice(0, 22)}`;
363
+ .slice(0, 20)}`;
358
364
  const clientOrderIds = Array.from({ length: count }, (_, index) => {
359
365
  const clientOrderId = `0x${groupKey}${index
360
366
  .toString(16)
@@ -2805,6 +2811,13 @@ export class HyperLiquidProvider {
2805
2811
  * Failure is non-blocking: order construction will use the ordinary builder
2806
2812
  * at the standard fee until a later approval succeeds.
2807
2813
  *
2814
+ * @deprecated ADR 0064 replaced the dedicated subscription builder with cloid
2815
+ * marking on the standard builder, so nothing reads this approval any more —
2816
+ * {@link #getBuilderOrderContext} never selects the subscription builder
2817
+ * address. The approval machinery is kept intact, unreachable, so the
2818
+ * previous design can be restored cheaply if cloid marking does not hold up
2819
+ * in shadow mode; remove it, the builder-address config, and
2820
+ * {@link #getSubscriptionBuilderAddress} once it does.
2808
2821
  * @returns Whether the builder is approved for the current account.
2809
2822
  */
2810
2823
  async approveSubscriptionBuilderFee() {
@@ -3540,7 +3553,7 @@ export class HyperLiquidProvider {
3540
3553
  });
3541
3554
  try {
3542
3555
  const result = await exchangeClient.order({
3543
- orders,
3556
+ orders: this.#applySubscriptionCloid(orders),
3544
3557
  grouping,
3545
3558
  ...(builder && { builder }),
3546
3559
  });
@@ -4295,16 +4308,27 @@ export class HyperLiquidProvider {
4295
4308
  }
4296
4309
  const { prices, sizes } = ladder;
4297
4310
  const count = prices.length;
4298
- const { groupId, clientOrderIds } = createScaleOrderIdentity(count);
4299
- const orders = prices.map((price, index) => ({
4311
+ const { clientOrderIds: ladderClientOrderIds } = createScaleOrderIdentity(count);
4312
+ // Marked here rather than at submission so the ids recorded for
4313
+ // cancel-by-cloid are exactly the ids the venue received. Marking keeps the
4314
+ // Scale group marker and the rung index, so the group stays recoverable.
4315
+ const orders = this.#applySubscriptionCloid(prices.map((price, index) => ({
4300
4316
  a: assetId,
4301
4317
  b: params.isBuy,
4302
4318
  p: price,
4303
4319
  s: sizes[index],
4304
4320
  r: params.reduceOnly ?? false,
4305
4321
  t: { limit: { tif: 'Gtc' } },
4306
- c: clientOrderIds[index],
4307
- }));
4322
+ c: ladderClientOrderIds[index],
4323
+ })),
4324
+ // These ids were generated a few lines above, so they are ours to stamp.
4325
+ new Set(ladderClientOrderIds));
4326
+ const clientOrderIds = orders.map((order, index) => order.c === undefined ? ladderClientOrderIds[index] : order.c);
4327
+ // Derived from the submitted ids, not from the pre-marking identity: the
4328
+ // group is recovered from open orders by reading their cloids, so tracking
4329
+ // it under any other key would register the same ladder twice.
4330
+ const groupId = readScaleGroupId(clientOrderIds[0]) ??
4331
+ `scale:${clientOrderIds[0].slice(2, -2)}`;
4308
4332
  this.#deps.debugLogger.log('Submitting scale ladder', {
4309
4333
  symbol: params.symbol,
4310
4334
  assetId,
@@ -4495,6 +4519,10 @@ export class HyperLiquidProvider {
4495
4519
  */
4496
4520
  async #startChaseSession(params, context, generation) {
4497
4521
  const { assetId, szDecimals, formattedSize, builder } = context;
4522
+ // Captured now, alongside the builder fee and for the same reason: the fee
4523
+ // resolution behind it is cleared when the caller's `placeOrder` returns,
4524
+ // which for a chase is before any replacement runs.
4525
+ const marksSubscriptionCloid = this.#isSubscriptionFeeSource();
4498
4526
  // The preamble is several round trips long. A disconnect during it has
4499
4527
  // already torn down everything this session would run on, so the chase
4500
4528
  // stops here rather than reading the book and putting a fresh order on it
@@ -4537,6 +4565,7 @@ export class HyperLiquidProvider {
4537
4565
  size: formattedSize,
4538
4566
  reduceOnly: params.reduceOnly ?? false,
4539
4567
  builder,
4568
+ marksSubscriptionCloid,
4540
4569
  exchangeClient: placingClient,
4541
4570
  });
4542
4571
  break;
@@ -4587,6 +4616,7 @@ export class HyperLiquidProvider {
4587
4616
  intervalMs,
4588
4617
  lastSnapshotSizeRefreshAt: 0,
4589
4618
  builder,
4619
+ marksSubscriptionCloid,
4590
4620
  deadline: params.chaseMaxDurationMs === undefined
4591
4621
  ? Number.POSITIVE_INFINITY
4592
4622
  : Date.now() + params.chaseMaxDurationMs,
@@ -4696,6 +4726,9 @@ export class HyperLiquidProvider {
4696
4726
  * @param params.size - Formatted size.
4697
4727
  * @param params.reduceOnly - Whether the order may only reduce a position.
4698
4728
  * @param params.builder - Builder context captured when the session started.
4729
+ * @param params.marksSubscriptionCloid - Whether to mark the cloid, captured
4730
+ * when the session started. Passed explicitly rather than read live: a
4731
+ * replacement runs long after the fee resolution behind it was cleared.
4699
4732
  * @param params.exchangeClient - Client to submit through. Passed in rather
4700
4733
  * than looked up here so a first placement can keep the instance it signed
4701
4734
  * with, which is the only one that can take the order back once `disconnect`
@@ -4704,18 +4737,18 @@ export class HyperLiquidProvider {
4704
4737
  */
4705
4738
  async #restChaseOrder(params) {
4706
4739
  const result = await params.exchangeClient.order({
4707
- orders: [
4740
+ orders: this.#applySubscriptionCloid([
4708
4741
  {
4709
4742
  a: params.assetId,
4710
4743
  b: params.isBuy,
4711
4744
  p: params.price,
4712
4745
  s: params.size,
4713
4746
  r: params.reduceOnly,
4714
- // Post-only: a chase adds liquidity at the touch. Crossing would end
4715
- // the chase on its first tick at a worse price than resting does.
4747
+ // Post-only: a chase adds liquidity at the touch. Crossing would
4748
+ // end the chase on its first tick at a worse price than resting.
4716
4749
  t: { limit: { tif: 'Alo' } },
4717
4750
  },
4718
- ],
4751
+ ], undefined, params.marksSubscriptionCloid),
4719
4752
  grouping: 'na',
4720
4753
  ...(params.builder && { builder: params.builder }),
4721
4754
  });
@@ -5054,6 +5087,7 @@ export class HyperLiquidProvider {
5054
5087
  size: remaining,
5055
5088
  reduceOnly: session.reduceOnly,
5056
5089
  builder: session.builder,
5090
+ marksSubscriptionCloid: session.marksSubscriptionCloid,
5057
5091
  // A running session is on a live provider, so the current client is
5058
5092
  // the right one; only the first placement has a teardown to survive.
5059
5093
  exchangeClient: this.#clientService.getExchangeClient(),
@@ -6182,34 +6216,86 @@ export class HyperLiquidProvider {
6182
6216
  if (this.#userFeeDiscountBips === undefined) {
6183
6217
  return BUILDER_FEE_CONFIG.MaxFeeTenthsBps;
6184
6218
  }
6185
- return Math.floor(BUILDER_FEE_CONFIG.MaxFeeTenthsBps *
6186
- (1 - this.#userFeeDiscountBips / BASIS_POINTS_DIVISOR));
6219
+ // Shared with the preview so a quoted rate is the rate the venue charges.
6220
+ return quantizeBuilderFeeTenthsBps(this.#userFeeDiscountBips);
6187
6221
  }
6188
6222
  /**
6189
6223
  * Resolve the builder payload for the current operation.
6190
6224
  *
6191
- * Subscription waivers use their dedicated builder only after approval is
6192
- * cached for this provider/account session. Until then, the ordinary builder
6193
- * and standard fee keep the trade attributable and non-blocking.
6225
+ * ADR 0064 removed the dedicated subscription builder: every source — the
6226
+ * subscription waiver included — now pays through the standard builder at the
6227
+ * resolved fee, and the subscription attribution rides on the order's cloid
6228
+ * instead (see {@link #applySubscriptionCloid}). That drops the per-user
6229
+ * builder approval the ADR rejected, and lets a partial waiver charge a real
6230
+ * blended fee, which a dedicated 0-bips builder could not express.
6194
6231
  *
6195
6232
  * @param setupContext - Account, network, and builder approved for the order.
6196
6233
  * @returns HyperLiquid builder address and fee payload.
6197
6234
  */
6198
6235
  async #getBuilderOrderContext(setupContext) {
6199
- const { network, userAddress, builderAddress: defaultBuilder, } = setupContext;
6200
- const isTestnet = network === 'testnet';
6201
- if (this.#userFeeResolution?.source === 'subscription') {
6202
- const subscriptionBuilder = this.#getSubscriptionBuilderAddress(isTestnet);
6203
- if (subscriptionBuilder &&
6204
- this.#approvedBuilderAddresses.has(this.#getApprovedBuilderKey(network, userAddress, subscriptionBuilder))) {
6205
- return { b: subscriptionBuilder, f: 0 };
6206
- }
6207
- return {
6208
- b: defaultBuilder,
6209
- f: BUILDER_FEE_CONFIG.MaxFeeTenthsBps,
6210
- };
6236
+ return {
6237
+ b: setupContext.builderAddress,
6238
+ f: this.#getDiscountedBuilderFee(),
6239
+ };
6240
+ }
6241
+ /**
6242
+ * Whether the operation in flight actually carries a subscription reduction.
6243
+ *
6244
+ * Winning the comparison is not sufficient. A nearly-spent allowance produces
6245
+ * a blend that approaches the full fee without reaching it, so it still wins
6246
+ * on `<=` — but its discount rounds to zero bips and
6247
+ * {@link #getDiscountedBuilderFee} then charges the undiscounted fee. Marking
6248
+ * such an order would tell the fill fan-out a waiver applied when the user
6249
+ * paid full price. The marking therefore follows the charged fee, not the
6250
+ * winning source.
6251
+ *
6252
+ * @returns True when the resolved source is `subscription` and it reduced the fee.
6253
+ */
6254
+ #isSubscriptionFeeSource() {
6255
+ return (this.#userFeeResolution?.source === 'subscription' &&
6256
+ this.#getDiscountedBuilderFee() < BUILDER_FEE_CONFIG.MaxFeeTenthsBps);
6257
+ }
6258
+ /**
6259
+ * Mark an order's cloid for the subscription program when it won the fee.
6260
+ *
6261
+ * This is the single place a cloid becomes subscription-attributed. Every
6262
+ * placement path — primary submit, scale ladder, TP/SL (attached and
6263
+ * standalone), batch close, modify/replace, and chase — routes its orders
6264
+ * through here, so no path can silently ship an unmarked order while the
6265
+ * waiver is being charged, and no other fee source can produce a marked one.
6266
+ *
6267
+ * A cloid this package generated keeps its trailing entropy, so the Scale
6268
+ * ladder's per-rung index and its cancel-by-cloid recovery survive marking.
6269
+ * A caller's `OrderParams.clientOrderId` is never rewritten, so those orders
6270
+ * go unattributed — which is why `isGenerated` is passed explicitly rather
6271
+ * than inferred from the id's leading bytes: a caller is free to supply one
6272
+ * that happens to begin with a reserved marker.
6273
+ *
6274
+ * @param orders - The SDK order payloads about to be submitted.
6275
+ * @param generatedCloids - Cloids this package generated for these orders and
6276
+ * may therefore re-stamp. Only the Scale ladder supplies any; every other path
6277
+ * either has no cloid or carries the caller's own.
6278
+ * @param marksSubscriptionCloid - Overrides the live fee resolution for a
6279
+ * chase replacement, which runs after that resolution has been cleared.
6280
+ * @returns The same payloads, with cloids marked when subscription won.
6281
+ */
6282
+ #applySubscriptionCloid(orders, generatedCloids, marksSubscriptionCloid) {
6283
+ // A chase replacement decides from the session, because the live resolution
6284
+ // was cleared when the caller's `placeOrder` returned. Everything else
6285
+ // decides from the resolution in flight.
6286
+ const marks = marksSubscriptionCloid ?? this.#isSubscriptionFeeSource();
6287
+ if (!marks) {
6288
+ // Any other source leaves the id exactly as the caller built it.
6289
+ return orders;
6211
6290
  }
6212
- return { b: defaultBuilder, f: this.#getDiscountedBuilderFee() };
6291
+ return orders.map((order) => ({
6292
+ ...order,
6293
+ c: markSubscriptionCloid({
6294
+ clientOrderId: order.c ?? undefined,
6295
+ isGenerated: order.c !== undefined && Boolean(generatedCloids?.has(order.c)),
6296
+ entropy: uuidv4().replace(/-/gu, ''),
6297
+ }),
6298
+ }));
6213
6299
  }
6214
6300
  /**
6215
6301
  * Read the account's currently resting orders.
@@ -6448,7 +6534,11 @@ export class HyperLiquidProvider {
6448
6534
  // HyperLiquid's modify action has no builder field and must not request a
6449
6535
  // builder-fee approval.
6450
6536
  await this.#ensureReadyForTrading({ requiresBuilderFee: false });
6451
- // Submit modification via SDK
6537
+ // Submit modification via SDK. The cloid is deliberately left unmarked:
6538
+ // `modify` carries no builder field, as the readiness call above records,
6539
+ // so no MetaMask fee is charged on this action and marking it would tell
6540
+ // the fill fan-out a reduction applied to an order that paid nothing.
6541
+ // The replacement inherits the resting order's own attribution.
6452
6542
  const exchangeClient = this.#clientService.getExchangeClient();
6453
6543
  const result = await exchangeClient.modify({
6454
6544
  oid: typeof params.orderId === 'string'
@@ -6895,7 +6985,7 @@ export class HyperLiquidProvider {
6895
6985
  : undefined;
6896
6986
  // Single batch API call
6897
6987
  const result = await exchangeClient.order({
6898
- orders,
6988
+ orders: this.#applySubscriptionCloid(orders),
6899
6989
  grouping: 'na',
6900
6990
  ...(builder && { builder }),
6901
6991
  });
@@ -7380,7 +7470,7 @@ export class HyperLiquidProvider {
7380
7470
  }
7381
7471
  try {
7382
7472
  const result = await exchangeClient.order({
7383
- orders: entries.map((entry) => entry.order),
7473
+ orders: this.#applySubscriptionCloid(entries.map((entry) => entry.order)),
7384
7474
  grouping: protection.grouping,
7385
7475
  ...(entries.some((entry) => entry.chargesMetamaskBuilderFee) &&
7386
7476
  builderOrderContext && { builder: builderOrderContext }),
@@ -7481,7 +7571,7 @@ export class HyperLiquidProvider {
7481
7571
  let result;
7482
7572
  try {
7483
7573
  result = await exchangeClient.order({
7484
- orders,
7574
+ orders: this.#applySubscriptionCloid(orders),
7485
7575
  grouping: isPartialTpsl ? 'na' : 'positionTpsl',
7486
7576
  ...(replacementChargesMetamaskBuilderFee &&
7487
7577
  builderOrderContext && { builder: builderOrderContext }),
@@ -10729,6 +10819,7 @@ export class HyperLiquidProvider {
10729
10819
  // MetaMask fees
10730
10820
  metamaskFeeRate,
10731
10821
  metamaskFeeAmount,
10822
+ chargesMetamaskBuilderFee,
10732
10823
  };
10733
10824
  this.#deps.debugLogger.log('Final Fee Calculation Result', {
10734
10825
  orderType,
@@ -11058,6 +11149,14 @@ export class HyperLiquidProvider {
11058
11149
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
11059
11150
  return this.#builderAddressMainnet || BUILDER_FEE_CONFIG.MainnetBuilder;
11060
11151
  }
11152
+ /**
11153
+ * The dedicated subscription builder address for this network, if configured.
11154
+ *
11155
+ * @deprecated Only {@link approveSubscriptionBuilderFee} still reads this;
11156
+ * order construction no longer does. See that method for the removal plan.
11157
+ * @param isTestnet - Whether the provider is in testnet mode.
11158
+ * @returns The configured address, or undefined.
11159
+ */
11061
11160
  #getSubscriptionBuilderAddress(isTestnet) {
11062
11161
  return isTestnet
11063
11162
  ? this.#subscriptionBuilderAddressTestnet