@metamask-previews/perps-controller 11.0.0-preview-9efd0b754 → 12.0.0-preview-4e64b66e8

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 (134) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/PerpsController-method-action-types.cjs.map +1 -1
  3. package/dist/PerpsController-method-action-types.d.cts +36 -1
  4. package/dist/PerpsController-method-action-types.d.cts.map +1 -1
  5. package/dist/PerpsController-method-action-types.d.mts +36 -1
  6. package/dist/PerpsController-method-action-types.d.mts.map +1 -1
  7. package/dist/PerpsController-method-action-types.mjs.map +1 -1
  8. package/dist/PerpsController.cjs +600 -119
  9. package/dist/PerpsController.cjs.map +1 -1
  10. package/dist/PerpsController.d.cts +35 -4
  11. package/dist/PerpsController.d.cts.map +1 -1
  12. package/dist/PerpsController.d.mts +35 -4
  13. package/dist/PerpsController.d.mts.map +1 -1
  14. package/dist/PerpsController.mjs +603 -122
  15. package/dist/PerpsController.mjs.map +1 -1
  16. package/dist/constants/eventNames.cjs +3 -0
  17. package/dist/constants/eventNames.cjs.map +1 -1
  18. package/dist/constants/eventNames.d.cts +1 -0
  19. package/dist/constants/eventNames.d.cts.map +1 -1
  20. package/dist/constants/eventNames.d.mts +1 -0
  21. package/dist/constants/eventNames.d.mts.map +1 -1
  22. package/dist/constants/eventNames.mjs +3 -0
  23. package/dist/constants/eventNames.mjs.map +1 -1
  24. package/dist/constants/hyperLiquidConfig.cjs +14 -2
  25. package/dist/constants/hyperLiquidConfig.cjs.map +1 -1
  26. package/dist/constants/hyperLiquidConfig.d.cts +7 -0
  27. package/dist/constants/hyperLiquidConfig.d.cts.map +1 -1
  28. package/dist/constants/hyperLiquidConfig.d.mts +7 -0
  29. package/dist/constants/hyperLiquidConfig.d.mts.map +1 -1
  30. package/dist/constants/hyperLiquidConfig.mjs +11 -0
  31. package/dist/constants/hyperLiquidConfig.mjs.map +1 -1
  32. package/dist/constants/perpsConfig.cjs +19 -2
  33. package/dist/constants/perpsConfig.cjs.map +1 -1
  34. package/dist/constants/perpsConfig.d.cts +39 -7
  35. package/dist/constants/perpsConfig.d.cts.map +1 -1
  36. package/dist/constants/perpsConfig.d.mts +39 -7
  37. package/dist/constants/perpsConfig.d.mts.map +1 -1
  38. package/dist/constants/perpsConfig.mjs +18 -1
  39. package/dist/constants/perpsConfig.mjs.map +1 -1
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +3 -3
  42. package/dist/index.d.cts.map +1 -1
  43. package/dist/index.d.mts +3 -3
  44. package/dist/index.d.mts.map +1 -1
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/providers/AggregatedPerpsProvider.cjs +16 -0
  47. package/dist/providers/AggregatedPerpsProvider.cjs.map +1 -1
  48. package/dist/providers/AggregatedPerpsProvider.d.cts +3 -1
  49. package/dist/providers/AggregatedPerpsProvider.d.cts.map +1 -1
  50. package/dist/providers/AggregatedPerpsProvider.d.mts +3 -1
  51. package/dist/providers/AggregatedPerpsProvider.d.mts.map +1 -1
  52. package/dist/providers/AggregatedPerpsProvider.mjs +16 -0
  53. package/dist/providers/AggregatedPerpsProvider.mjs.map +1 -1
  54. package/dist/providers/HyperLiquidProvider.cjs +325 -59
  55. package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
  56. package/dist/providers/HyperLiquidProvider.d.cts +27 -1
  57. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
  58. package/dist/providers/HyperLiquidProvider.d.mts +27 -1
  59. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
  60. package/dist/providers/HyperLiquidProvider.mjs +326 -60
  61. package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
  62. package/dist/services/HyperLiquidClientService.cjs +74 -25
  63. package/dist/services/HyperLiquidClientService.cjs.map +1 -1
  64. package/dist/services/HyperLiquidClientService.d.cts.map +1 -1
  65. package/dist/services/HyperLiquidClientService.d.mts.map +1 -1
  66. package/dist/services/HyperLiquidClientService.mjs +74 -25
  67. package/dist/services/HyperLiquidClientService.mjs.map +1 -1
  68. package/dist/services/HyperLiquidSubscriptionService.cjs +16 -5
  69. package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
  70. package/dist/services/HyperLiquidSubscriptionService.d.cts +1 -1
  71. package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
  72. package/dist/services/HyperLiquidSubscriptionService.d.mts +1 -1
  73. package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
  74. package/dist/services/HyperLiquidSubscriptionService.mjs +16 -5
  75. package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
  76. package/dist/services/MarketDataService.cjs +49 -5
  77. package/dist/services/MarketDataService.cjs.map +1 -1
  78. package/dist/services/MarketDataService.d.cts +2 -2
  79. package/dist/services/MarketDataService.d.cts.map +1 -1
  80. package/dist/services/MarketDataService.d.mts +2 -2
  81. package/dist/services/MarketDataService.d.mts.map +1 -1
  82. package/dist/services/MarketDataService.mjs +49 -5
  83. package/dist/services/MarketDataService.mjs.map +1 -1
  84. package/dist/services/RewardsIntegrationService.cjs +330 -68
  85. package/dist/services/RewardsIntegrationService.cjs.map +1 -1
  86. package/dist/services/RewardsIntegrationService.d.cts +61 -5
  87. package/dist/services/RewardsIntegrationService.d.cts.map +1 -1
  88. package/dist/services/RewardsIntegrationService.d.mts +61 -5
  89. package/dist/services/RewardsIntegrationService.d.mts.map +1 -1
  90. package/dist/services/RewardsIntegrationService.mjs +330 -68
  91. package/dist/services/RewardsIntegrationService.mjs.map +1 -1
  92. package/dist/services/ServiceContext.cjs.map +1 -1
  93. package/dist/services/ServiceContext.d.cts +17 -1
  94. package/dist/services/ServiceContext.d.cts.map +1 -1
  95. package/dist/services/ServiceContext.d.mts +17 -1
  96. package/dist/services/ServiceContext.d.mts.map +1 -1
  97. package/dist/services/ServiceContext.mjs.map +1 -1
  98. package/dist/services/TerminalMarketService.cjs +422 -7
  99. package/dist/services/TerminalMarketService.cjs.map +1 -1
  100. package/dist/services/TerminalMarketService.d.cts +10 -1
  101. package/dist/services/TerminalMarketService.d.cts.map +1 -1
  102. package/dist/services/TerminalMarketService.d.mts +10 -1
  103. package/dist/services/TerminalMarketService.d.mts.map +1 -1
  104. package/dist/services/TerminalMarketService.mjs +423 -8
  105. package/dist/services/TerminalMarketService.mjs.map +1 -1
  106. package/dist/services/TradingService.cjs +52 -28
  107. package/dist/services/TradingService.cjs.map +1 -1
  108. package/dist/services/TradingService.d.cts.map +1 -1
  109. package/dist/services/TradingService.d.mts.map +1 -1
  110. package/dist/services/TradingService.mjs +52 -28
  111. package/dist/services/TradingService.mjs.map +1 -1
  112. package/dist/types/index.cjs.map +1 -1
  113. package/dist/types/index.d.cts +156 -7
  114. package/dist/types/index.d.cts.map +1 -1
  115. package/dist/types/index.d.mts +156 -7
  116. package/dist/types/index.d.mts.map +1 -1
  117. package/dist/types/index.mjs.map +1 -1
  118. package/dist/utils/marketUtils.cjs +16 -1
  119. package/dist/utils/marketUtils.cjs.map +1 -1
  120. package/dist/utils/marketUtils.d.cts +1 -0
  121. package/dist/utils/marketUtils.d.cts.map +1 -1
  122. package/dist/utils/marketUtils.d.mts +1 -0
  123. package/dist/utils/marketUtils.d.mts.map +1 -1
  124. package/dist/utils/marketUtils.mjs +14 -0
  125. package/dist/utils/marketUtils.mjs.map +1 -1
  126. package/dist/utils/perpsDiskPersistence.cjs +19 -12
  127. package/dist/utils/perpsDiskPersistence.cjs.map +1 -1
  128. package/dist/utils/perpsDiskPersistence.d.cts +5 -1
  129. package/dist/utils/perpsDiskPersistence.d.cts.map +1 -1
  130. package/dist/utils/perpsDiskPersistence.d.mts +5 -1
  131. package/dist/utils/perpsDiskPersistence.d.mts.map +1 -1
  132. package/dist/utils/perpsDiskPersistence.mjs +19 -12
  133. package/dist/utils/perpsDiskPersistence.mjs.map +1 -1
  134. package/package.json +1 -1
@@ -1,10 +1,27 @@
1
- import type { PerpsPlatformDependencies } from "../types/index.mjs";
1
+ import type { PerpsFeeResolution, PerpsPlatformDependencies, PerpsSubscriptionFeeWaiverStatus } from "../types/index.mjs";
2
2
  import type { PerpsControllerMessengerBase } from "../types/messenger.mjs";
3
3
  /**
4
4
  * RewardsIntegrationService
5
5
  *
6
- * Handles rewards-related operations and fee discount calculations.
7
- * Stateless service that coordinates with RewardsController and NetworkController.
6
+ * Owns the unified perps fee resolver: it considers every fee source and
7
+ * returns the lowest fee, expressed as the discount bips providers consume.
8
+ *
9
+ * Sources, all in fee basis points (lowest wins):
10
+ * - `default` — {@link BUILDER_FEE_CONFIG}, the fee with no reductions.
11
+ * - `rewards` — VIP and season, collapsed into one discount by
12
+ * `RewardsController` (`rewards.getPerpsDiscountForAccount`), so this service
13
+ * does not re-derive the VIP/season split.
14
+ * - `subscription` — `0` bips, but only when the eligibility gate passes on a
15
+ * cached read of the profile's benefits.
16
+ *
17
+ * On a tie the cheaper-to-explain source wins, in the order
18
+ * `subscription` > `rewards` > `default`.
19
+ *
20
+ * The benefits cache is stale-while-revalidate: fee resolution is a pure read
21
+ * of the cached snapshot, while preview and lifecycle callers refresh it
22
+ * explicitly. Nothing is reserved or committed client-side, so backend
23
+ * exhaustion needs no release logic — the next refresh simply stops passing
24
+ * the gate.
8
25
  *
9
26
  * Instance-based service with constructor injection of platform dependencies.
10
27
  */
@@ -18,11 +35,50 @@ export declare class RewardsIntegrationService {
18
35
  */
19
36
  constructor(deps: PerpsPlatformDependencies, messenger: PerpsControllerMessengerBase);
20
37
  /**
21
- * Calculate user fee discount from rewards
38
+ * Calculate user fee discount from the unified fee resolver.
22
39
  * Returns discount in basis points (e.g., 6500 = 65% discount)
23
40
  *
24
- * @returns The fee discount in basis points, or undefined if unavailable.
41
+ * @returns The fee discount in basis points, or undefined if no source resolved.
25
42
  */
26
43
  calculateUserFeeDiscount(): Promise<number | undefined>;
44
+ /**
45
+ * Resolve the MetaMask builder fee across every source and return the lowest.
46
+ *
47
+ * Never throws and never starts a subscription benefits read: a failing or
48
+ * unresolved cached source simply drops out of the comparison, so the worst
49
+ * case is the default fee rather than an error or an over-granted waiver.
50
+ *
51
+ * @returns The winning fee, its source, and the subscription gate outcome.
52
+ */
53
+ resolveFee(): Promise<PerpsFeeResolution>;
54
+ /**
55
+ * Read the subscription fee-waiver gate from the cached benefits snapshot.
56
+ *
57
+ * Synchronous and side-effect free. The returned value always comes from
58
+ * what is already cached; preview and lifecycle callers own hydration.
59
+ *
60
+ * @returns Whether the waiver applies, why, and the remaining notional.
61
+ */
62
+ getSubscriptionFeeWaiverStatus(): PerpsSubscriptionFeeWaiverStatus;
63
+ /**
64
+ * Refresh the cached subscription benefits snapshot.
65
+ *
66
+ * Deduped: concurrent callers share the in-flight request. Rejections are
67
+ * logged and swallowed, leaving the previous snapshot in place. Preview and
68
+ * lifecycle callers invoke this outside order submission.
69
+ *
70
+ * @returns A promise that settles when the refresh completes.
71
+ */
72
+ refreshSubscriptionBenefits(): Promise<void>;
73
+ /**
74
+ * Drop the cached benefits snapshot.
75
+ *
76
+ * Call this when the identity behind the benefits changes — sign-out, or a
77
+ * profile switch — since the snapshot carries no profile identity of its own
78
+ * and would otherwise keep answering for the previous profile until the next
79
+ * successful refresh. The next status read reports `not-hydrated`, so the
80
+ * waiver is withheld until a preview or lifecycle caller hydrates it.
81
+ */
82
+ invalidateSubscriptionBenefits(): void;
27
83
  }
28
84
  //# sourceMappingURL=RewardsIntegrationService.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RewardsIntegrationService.d.mts","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,yBAAyB,EAAE,2BAA0B;AACnE,OAAO,KAAK,EAAE,4BAA4B,EAAE,+BAA8B;AAK1E;;;;;;;GAOG;AACH,qBAAa,yBAAyB;;IAKpC;;;;;OAKG;gBAED,IAAI,EAAE,yBAAyB,EAC/B,SAAS,EAAE,4BAA4B;IAyBzC;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAuG9D"}
1
+ {"version":3,"file":"RewardsIntegrationService.d.mts","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,kBAAkB,EAElB,yBAAyB,EAEzB,gCAAgC,EACjC,2BAA0B;AAC3B,OAAO,KAAK,EAAE,4BAA4B,EAAE,+BAA8B;AAoB1E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,yBAAyB;;IA8BpC;;;;;OAKG;gBAED,IAAI,EAAE,yBAAyB,EAC/B,SAAS,EAAE,4BAA4B;IAyBzC;;;;;OAKG;IACG,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK7D;;;;;;;;OAQG;IACG,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC;IA4C/C;;;;;;;OAOG;IACH,8BAA8B,IAAI,gCAAgC;IAqBlE;;;;;;;;OAQG;IACG,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;IAsClD;;;;;;;;OAQG;IACH,8BAA8B,IAAI,IAAI;CA0LvC"}
@@ -9,17 +9,39 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _RewardsIntegrationService_instances, _RewardsIntegrationService_deps, _RewardsIntegrationService_messenger, _RewardsIntegrationService_getChainIdForNetwork;
12
+ var _RewardsIntegrationService_instances, _RewardsIntegrationService_deps, _RewardsIntegrationService_messenger, _RewardsIntegrationService_benefitsSnapshot, _RewardsIntegrationService_lastAttemptAt, _RewardsIntegrationService_benefitsRefresh, _RewardsIntegrationService_benefitsEpoch, _RewardsIntegrationService_getChainIdForNetwork, _RewardsIntegrationService_readSubscriptionBenefits, _RewardsIntegrationService_calculateRewardsDiscount;
13
13
  import { BASIS_POINTS_DIVISOR, BUILDER_FEE_CONFIG } from "../constants/hyperLiquidConfig.mjs";
14
- import { PERPS_CONSTANTS } from "../constants/perpsConfig.mjs";
14
+ import { PERPS_CONSTANTS, SUBSCRIPTION_BENEFITS_CACHE } from "../constants/perpsConfig.mjs";
15
15
  import { getSelectedEvmAccountFromMessenger } from "../utils/accountUtils.mjs";
16
16
  import { ensureError } from "../utils/errorUtils.mjs";
17
17
  import { formatAccountToCaipAccountId } from "../utils/rewardsUtils.mjs";
18
+ /**
19
+ * Default MetaMask builder fee, in basis points.
20
+ * This is the fee every user pays when no cheaper source applies.
21
+ */
22
+ const DEFAULT_FEE_BIPS = BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR;
18
23
  /**
19
24
  * RewardsIntegrationService
20
25
  *
21
- * Handles rewards-related operations and fee discount calculations.
22
- * Stateless service that coordinates with RewardsController and NetworkController.
26
+ * Owns the unified perps fee resolver: it considers every fee source and
27
+ * returns the lowest fee, expressed as the discount bips providers consume.
28
+ *
29
+ * Sources, all in fee basis points (lowest wins):
30
+ * - `default` — {@link BUILDER_FEE_CONFIG}, the fee with no reductions.
31
+ * - `rewards` — VIP and season, collapsed into one discount by
32
+ * `RewardsController` (`rewards.getPerpsDiscountForAccount`), so this service
33
+ * does not re-derive the VIP/season split.
34
+ * - `subscription` — `0` bips, but only when the eligibility gate passes on a
35
+ * cached read of the profile's benefits.
36
+ *
37
+ * On a tie the cheaper-to-explain source wins, in the order
38
+ * `subscription` > `rewards` > `default`.
39
+ *
40
+ * The benefits cache is stale-while-revalidate: fee resolution is a pure read
41
+ * of the cached snapshot, while preview and lifecycle callers refresh it
42
+ * explicitly. Nothing is reserved or committed client-side, so backend
43
+ * exhaustion needs no release logic — the next refresh simply stops passing
44
+ * the gate.
23
45
  *
24
46
  * Instance-based service with constructor injection of platform dependencies.
25
47
  */
@@ -34,92 +56,332 @@ export class RewardsIntegrationService {
34
56
  _RewardsIntegrationService_instances.add(this);
35
57
  _RewardsIntegrationService_deps.set(this, void 0);
36
58
  _RewardsIntegrationService_messenger.set(this, void 0);
59
+ /** Last successful benefits read, or undefined before the first one. */
60
+ _RewardsIntegrationService_benefitsSnapshot.set(this, void 0);
61
+ /**
62
+ * When the last benefits read finished, successful or not.
63
+ *
64
+ * Separate from `#benefitsSnapshot.fetchedAt`, which only advances on
65
+ * success: a failing read must still throttle the next preview refresh,
66
+ * otherwise an outage turns every fee preview into a new request.
67
+ */
68
+ _RewardsIntegrationService_lastAttemptAt.set(this, void 0);
69
+ /** In-flight refresh, deduped so only one runs at a time. */
70
+ _RewardsIntegrationService_benefitsRefresh.set(this, void 0);
71
+ /**
72
+ * Identity generation for the cached benefits.
73
+ *
74
+ * Bumped by {@link invalidateSubscriptionBenefits}; a read that resolves
75
+ * against a superseded epoch is discarded rather than written back, so a
76
+ * refresh issued for the previous profile cannot repopulate the cache after
77
+ * a sign-out or profile switch.
78
+ */
79
+ _RewardsIntegrationService_benefitsEpoch.set(this, 0);
37
80
  __classPrivateFieldSet(this, _RewardsIntegrationService_deps, deps, "f");
38
81
  __classPrivateFieldSet(this, _RewardsIntegrationService_messenger, messenger, "f");
39
82
  }
40
83
  /**
41
- * Calculate user fee discount from rewards
84
+ * Calculate user fee discount from the unified fee resolver.
42
85
  * Returns discount in basis points (e.g., 6500 = 65% discount)
43
86
  *
44
- * @returns The fee discount in basis points, or undefined if unavailable.
87
+ * @returns The fee discount in basis points, or undefined if no source resolved.
45
88
  */
46
89
  async calculateUserFeeDiscount() {
47
- try {
48
- const evmAccount = getSelectedEvmAccountFromMessenger(__classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f"));
49
- if (!evmAccount) {
50
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: No EVM account found for fee discount');
51
- return undefined;
90
+ const resolution = await this.resolveFee();
91
+ return resolution.discountBips;
92
+ }
93
+ /**
94
+ * Resolve the MetaMask builder fee across every source and return the lowest.
95
+ *
96
+ * Never throws and never starts a subscription benefits read: a failing or
97
+ * unresolved cached source simply drops out of the comparison, so the worst
98
+ * case is the default fee rather than an error or an over-granted waiver.
99
+ *
100
+ * @returns The winning fee, its source, and the subscription gate outcome.
101
+ */
102
+ async resolveFee() {
103
+ const rewardsDiscountBips = await __classPrivateFieldGet(this, _RewardsIntegrationService_instances, "m", _RewardsIntegrationService_calculateRewardsDiscount).call(this);
104
+ // Pure cache read: subscription benefits must never start a network request
105
+ // while an order is being prepared for signing.
106
+ const subscription = this.getSubscriptionFeeWaiverStatus();
107
+ let feeBips = DEFAULT_FEE_BIPS;
108
+ let source = 'default';
109
+ if (rewardsDiscountBips !== undefined) {
110
+ const rewardsFeeBips = DEFAULT_FEE_BIPS * (1 - rewardsDiscountBips / BASIS_POINTS_DIVISOR);
111
+ // `<=` so an equal rewards fee still reports the rewards source, keeping
112
+ // a resolved 0% discount distinguishable from an unresolved one.
113
+ if (rewardsFeeBips <= feeBips) {
114
+ feeBips = rewardsFeeBips;
115
+ source = 'rewards';
52
116
  }
53
- // Get the chain ID via DI network controller
54
- const networkState = __classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f").call('NetworkController:getState');
55
- const { selectedNetworkClientId } = networkState;
56
- const chainId = __classPrivateFieldGet(this, _RewardsIntegrationService_instances, "m", _RewardsIntegrationService_getChainIdForNetwork).call(this, selectedNetworkClientId);
57
- if (!chainId) {
58
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(new Error('Chain ID not found for fee discount calculation'), {
59
- tags: { feature: PERPS_CONSTANTS.FeatureName },
60
- context: {
61
- name: 'RewardsIntegrationService.calculateUserFeeDiscount',
62
- data: {
63
- selectedNetworkClientId,
64
- },
65
- },
66
- });
67
- return undefined;
117
+ }
118
+ // Nothing can undercut a waived fee, so the gate passing always wins.
119
+ if (subscription.eligible) {
120
+ feeBips = 0;
121
+ source = 'subscription';
122
+ }
123
+ const discountBips = source === 'default'
124
+ ? undefined
125
+ : Math.round((1 - feeBips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR);
126
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Fee resolved', {
127
+ source,
128
+ feeBips,
129
+ discountBips,
130
+ defaultFeeBips: DEFAULT_FEE_BIPS,
131
+ rewardsDiscountBips,
132
+ subscriptionEligible: subscription.eligible,
133
+ subscriptionReason: subscription.reason,
134
+ });
135
+ return { feeBips, discountBips, source, subscription };
136
+ }
137
+ /**
138
+ * Read the subscription fee-waiver gate from the cached benefits snapshot.
139
+ *
140
+ * Synchronous and side-effect free. The returned value always comes from
141
+ * what is already cached; preview and lifecycle callers own hydration.
142
+ *
143
+ * @returns Whether the waiver applies, why, and the remaining notional.
144
+ */
145
+ getSubscriptionFeeWaiverStatus() {
146
+ if (!__classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").subscription) {
147
+ return { eligible: false, reason: 'no-source' };
148
+ }
149
+ const now = Date.now();
150
+ const snapshot = __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsSnapshot, "f");
151
+ const age = snapshot ? now - snapshot.fetchedAt : Infinity;
152
+ if (!snapshot) {
153
+ return { eligible: false, reason: 'not-hydrated' };
154
+ }
155
+ if (age > SUBSCRIPTION_BENEFITS_CACHE.MaxStaleMs) {
156
+ // Past the ceiling we cannot tell whether the cap is still available, so
157
+ // fall back to the next-lowest source rather than over-granting.
158
+ return { eligible: false, reason: 'stale' };
159
+ }
160
+ return evaluateFeeWaiverGate(snapshot.benefits);
161
+ }
162
+ /**
163
+ * Refresh the cached subscription benefits snapshot.
164
+ *
165
+ * Deduped: concurrent callers share the in-flight request. Rejections are
166
+ * logged and swallowed, leaving the previous snapshot in place. Preview and
167
+ * lifecycle callers invoke this outside order submission.
168
+ *
169
+ * @returns A promise that settles when the refresh completes.
170
+ */
171
+ async refreshSubscriptionBenefits() {
172
+ const source = __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").subscription;
173
+ if (!source) {
174
+ return;
175
+ }
176
+ if (__classPrivateFieldGet(this, _RewardsIntegrationService_benefitsRefresh, "f")) {
177
+ await __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsRefresh, "f");
178
+ return;
179
+ }
180
+ const now = Date.now();
181
+ const snapshotAge = __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsSnapshot, "f")
182
+ ? now - __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsSnapshot, "f").fetchedAt
183
+ : Infinity;
184
+ const sinceAttempt = __classPrivateFieldGet(this, _RewardsIntegrationService_lastAttemptAt, "f") === undefined ? Infinity : now - __classPrivateFieldGet(this, _RewardsIntegrationService_lastAttemptAt, "f");
185
+ if (snapshotAge < SUBSCRIPTION_BENEFITS_CACHE.FreshMs ||
186
+ sinceAttempt < SUBSCRIPTION_BENEFITS_CACHE.FreshMs) {
187
+ return;
188
+ }
189
+ const refresh = __classPrivateFieldGet(this, _RewardsIntegrationService_instances, "m", _RewardsIntegrationService_readSubscriptionBenefits).call(this, source);
190
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsRefresh, refresh, "f");
191
+ // `finally` always defers, so this never clears the handle we just set.
192
+ refresh
193
+ .finally(() => {
194
+ if (__classPrivateFieldGet(this, _RewardsIntegrationService_benefitsRefresh, "f") === refresh) {
195
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsRefresh, undefined, "f");
68
196
  }
69
- // Use pure utility function for CAIP formatting (pass logger for error reporting)
70
- const caipAccountId = formatAccountToCaipAccountId(evmAccount.address, chainId, __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger);
71
- if (!caipAccountId) {
72
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(new Error('Failed to format CAIP account ID for fee discount'), {
73
- tags: { feature: PERPS_CONSTANTS.FeatureName },
74
- context: {
75
- name: 'RewardsIntegrationService.calculateUserFeeDiscount',
76
- data: {
77
- address: evmAccount.address,
78
- chainId,
79
- selectedNetworkClientId,
80
- },
197
+ })
198
+ .catch(() => undefined);
199
+ await refresh;
200
+ }
201
+ /**
202
+ * Drop the cached benefits snapshot.
203
+ *
204
+ * Call this when the identity behind the benefits changes — sign-out, or a
205
+ * profile switch — since the snapshot carries no profile identity of its own
206
+ * and would otherwise keep answering for the previous profile until the next
207
+ * successful refresh. The next status read reports `not-hydrated`, so the
208
+ * waiver is withheld until a preview or lifecycle caller hydrates it.
209
+ */
210
+ invalidateSubscriptionBenefits() {
211
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsSnapshot, undefined, "f");
212
+ __classPrivateFieldSet(this, _RewardsIntegrationService_lastAttemptAt, undefined, "f");
213
+ // Fence any in-flight read: it was issued for the previous identity, so its
214
+ // result must not repopulate the cache after this point.
215
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsEpoch, __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsEpoch, "f") + 1, "f");
216
+ // Drop the dedupe handle too. The fenced read can only be discarded, so
217
+ // leaving it in place would make the next refresh await it instead of
218
+ // fetching for the new identity. Its `finally` guard compares against the
219
+ // current handle, so it will not clear whatever replaces it here.
220
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsRefresh, undefined, "f");
221
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Subscription benefits cache invalidated');
222
+ }
223
+ }
224
+ _RewardsIntegrationService_deps = new WeakMap(), _RewardsIntegrationService_messenger = new WeakMap(), _RewardsIntegrationService_benefitsSnapshot = new WeakMap(), _RewardsIntegrationService_lastAttemptAt = new WeakMap(), _RewardsIntegrationService_benefitsRefresh = new WeakMap(), _RewardsIntegrationService_benefitsEpoch = new WeakMap(), _RewardsIntegrationService_instances = new WeakSet(), _RewardsIntegrationService_getChainIdForNetwork = function _RewardsIntegrationService_getChainIdForNetwork(networkClientId) {
225
+ try {
226
+ const networkClient = __classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f").call('NetworkController:getNetworkClientById', networkClientId);
227
+ return networkClient.configuration.chainId;
228
+ }
229
+ catch {
230
+ // Network client may not exist
231
+ return undefined;
232
+ }
233
+ }, _RewardsIntegrationService_readSubscriptionBenefits =
234
+ /**
235
+ * Perform one benefits read and store it, keeping the previous snapshot on
236
+ * error. Never rejects, so callers cannot produce an unhandled rejection.
237
+ *
238
+ * @param source - The injected subscription benefits source.
239
+ */
240
+ async function _RewardsIntegrationService_readSubscriptionBenefits(source) {
241
+ const epoch = __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsEpoch, "f");
242
+ try {
243
+ const benefits = await source.getPerpsBenefits();
244
+ if (epoch !== __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsEpoch, "f")) {
245
+ // Invalidated while this read was in flight: it belongs to a previous
246
+ // identity, so discarding it is the only safe outcome.
247
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Discarding benefits read from a previous identity');
248
+ return;
249
+ }
250
+ __classPrivateFieldSet(this, _RewardsIntegrationService_benefitsSnapshot, { benefits, fetchedAt: Date.now() }, "f");
251
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Subscription benefits refreshed', {
252
+ status: benefits?.status,
253
+ entitled: benefits?.perpsFeeWaiver?.entitled,
254
+ usage: benefits?.perpsFeeWaiver?.usage,
255
+ exhausted: benefits?.perpsFeeWaiver?.exhausted,
256
+ });
257
+ }
258
+ catch (error) {
259
+ // Keep the previous snapshot: an unreachable benefits endpoint must not
260
+ // erase a valid cache, and it must never grant the waiver either.
261
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(ensureError(error, 'RewardsIntegrationService.refreshSubscriptionBenefits'), {
262
+ tags: { feature: PERPS_CONSTANTS.FeatureName },
263
+ context: {
264
+ name: 'RewardsIntegrationService.refreshSubscriptionBenefits',
265
+ data: {},
266
+ },
267
+ });
268
+ }
269
+ finally {
270
+ // Recorded on failure too — this is what throttles the retry loop. Not
271
+ // recorded for a fenced read: that attempt belongs to a previous
272
+ // identity, and letting it throttle would delay the new identity's first
273
+ // fetch by a whole freshness window.
274
+ if (epoch === __classPrivateFieldGet(this, _RewardsIntegrationService_benefitsEpoch, "f")) {
275
+ __classPrivateFieldSet(this, _RewardsIntegrationService_lastAttemptAt, Date.now(), "f");
276
+ }
277
+ }
278
+ }, _RewardsIntegrationService_calculateRewardsDiscount =
279
+ /**
280
+ * Resolve the rewards (VIP + season) discount for the selected account.
281
+ *
282
+ * @returns The discount in basis points, or undefined when unavailable.
283
+ */
284
+ async function _RewardsIntegrationService_calculateRewardsDiscount() {
285
+ try {
286
+ const evmAccount = getSelectedEvmAccountFromMessenger(__classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f"));
287
+ if (!evmAccount) {
288
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: No EVM account found for fee discount');
289
+ return undefined;
290
+ }
291
+ // Get the chain ID via DI network controller
292
+ const networkState = __classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f").call('NetworkController:getState');
293
+ const { selectedNetworkClientId } = networkState;
294
+ const chainId = __classPrivateFieldGet(this, _RewardsIntegrationService_instances, "m", _RewardsIntegrationService_getChainIdForNetwork).call(this, selectedNetworkClientId);
295
+ if (!chainId) {
296
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(new Error('Chain ID not found for fee discount calculation'), {
297
+ tags: { feature: PERPS_CONSTANTS.FeatureName },
298
+ context: {
299
+ name: 'RewardsIntegrationService.calculateUserFeeDiscount',
300
+ data: {
301
+ selectedNetworkClientId,
81
302
  },
82
- });
83
- return undefined;
84
- }
85
- // Use rewards via DI (no RewardsController in Core yet).
86
- // The rewards controller needs the perps MetaMask builder base fee in
87
- // bips to convert an absolute VIP fee into a discount fraction.
88
- const discountBips = await __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").rewards.getPerpsDiscountForAccount(caipAccountId, BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR);
89
- // null = subscription state not hydrated yet; surface as undefined so
90
- // callers don't treat it as a definitive "no discount" answer.
91
- if (discountBips === null) {
92
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Fee discount unavailable (subscription state not hydrated)', { address: evmAccount.address, caipAccountId });
93
- return undefined;
94
- }
95
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Fee discount calculated', {
96
- address: evmAccount.address,
97
- caipAccountId,
98
- discountBips,
99
- discountPercentage: discountBips / 100,
303
+ },
100
304
  });
101
- return discountBips;
305
+ return undefined;
102
306
  }
103
- catch (error) {
104
- __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(ensureError(error, 'RewardsIntegrationService.calculateUserFeeDiscount'), {
307
+ // Use pure utility function for CAIP formatting (pass logger for error reporting)
308
+ const caipAccountId = formatAccountToCaipAccountId(evmAccount.address, chainId, __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger);
309
+ if (!caipAccountId) {
310
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(new Error('Failed to format CAIP account ID for fee discount'), {
105
311
  tags: { feature: PERPS_CONSTANTS.FeatureName },
106
312
  context: {
107
313
  name: 'RewardsIntegrationService.calculateUserFeeDiscount',
108
- data: {},
314
+ data: {
315
+ address: evmAccount.address,
316
+ chainId,
317
+ selectedNetworkClientId,
318
+ },
109
319
  },
110
320
  });
111
321
  return undefined;
112
322
  }
323
+ // Use rewards via DI (no RewardsController in Core yet).
324
+ // The rewards controller needs the perps MetaMask builder base fee in
325
+ // bips to convert an absolute VIP fee into a discount fraction.
326
+ const discountBips = await __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").rewards.getPerpsDiscountForAccount(caipAccountId, DEFAULT_FEE_BIPS);
327
+ // null = subscription state not hydrated yet; surface as undefined so
328
+ // callers don't treat it as a definitive "no discount" answer.
329
+ if (discountBips === null) {
330
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Fee discount unavailable (subscription state not hydrated)', { address: evmAccount.address, caipAccountId });
331
+ return undefined;
332
+ }
333
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").debugLogger.log('RewardsIntegrationService: Fee discount calculated', {
334
+ address: evmAccount.address,
335
+ caipAccountId,
336
+ discountBips,
337
+ discountPercentage: discountBips / 100,
338
+ });
339
+ return discountBips;
113
340
  }
114
- }
115
- _RewardsIntegrationService_deps = new WeakMap(), _RewardsIntegrationService_messenger = new WeakMap(), _RewardsIntegrationService_instances = new WeakSet(), _RewardsIntegrationService_getChainIdForNetwork = function _RewardsIntegrationService_getChainIdForNetwork(networkClientId) {
116
- try {
117
- const networkClient = __classPrivateFieldGet(this, _RewardsIntegrationService_messenger, "f").call('NetworkController:getNetworkClientById', networkClientId);
118
- return networkClient.configuration.chainId;
119
- }
120
- catch {
121
- // Network client may not exist
341
+ catch (error) {
342
+ __classPrivateFieldGet(this, _RewardsIntegrationService_deps, "f").logger.error(ensureError(error, 'RewardsIntegrationService.calculateUserFeeDiscount'), {
343
+ tags: { feature: PERPS_CONSTANTS.FeatureName },
344
+ context: {
345
+ name: 'RewardsIntegrationService.calculateUserFeeDiscount',
346
+ data: {},
347
+ },
348
+ });
122
349
  return undefined;
123
350
  }
124
351
  };
352
+ /**
353
+ * Evaluate the perps fee-waiver eligibility gate against a benefits snapshot.
354
+ *
355
+ * The gate is `status=active` AND `perpsFeeWaiver` entitled AND
356
+ * `usage=available`. A backend `exhausted` flag (or an `exhausted` usage) fails
357
+ * the gate on its own; anything short of an affirmative `available` is treated
358
+ * as not entitled, because the waiver is only granted on positive evidence.
359
+ * A `null` payload means there is no subscription at all, which is reported
360
+ * separately from a subscription that exists but is not active.
361
+ *
362
+ * @param benefits - The cached benefits payload, or null when there is none.
363
+ * @returns The gate outcome plus the remaining notional when reported.
364
+ */
365
+ function evaluateFeeWaiverGate(benefits) {
366
+ const waiver = benefits?.perpsFeeWaiver;
367
+ const { remainingNotionalUsd } = waiver ?? {};
368
+ // `null` is the DI contract's "nothing to report" (signed out, no profile),
369
+ // which is distinct from a subscription that exists but is not active.
370
+ if (benefits === null) {
371
+ return { eligible: false, reason: 'no-subscription' };
372
+ }
373
+ if (benefits.status !== 'active') {
374
+ return { eligible: false, reason: 'inactive', remainingNotionalUsd };
375
+ }
376
+ if (waiver?.entitled !== true) {
377
+ return { eligible: false, reason: 'not-entitled', remainingNotionalUsd };
378
+ }
379
+ if (waiver.exhausted === true || waiver.usage === 'exhausted') {
380
+ return { eligible: false, reason: 'exhausted', remainingNotionalUsd };
381
+ }
382
+ if (waiver.usage !== 'available') {
383
+ return { eligible: false, reason: 'not-entitled', remainingNotionalUsd };
384
+ }
385
+ return { eligible: true, reason: 'eligible', remainingNotionalUsd };
386
+ }
125
387
  //# sourceMappingURL=RewardsIntegrationService.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RewardsIntegrationService.mjs","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BASIS_POINTS_DIVISOR,\n BUILDER_FEE_CONFIG,\n} from '../constants/hyperLiquidConfig.js';\nimport { PERPS_CONSTANTS } from '../constants/perpsConfig.js';\nimport type { PerpsPlatformDependencies } from '../types/index.js';\nimport type { PerpsControllerMessengerBase } from '../types/messenger.js';\nimport { getSelectedEvmAccountFromMessenger } from '../utils/accountUtils.js';\nimport { ensureError } from '../utils/errorUtils.js';\nimport { formatAccountToCaipAccountId } from '../utils/rewardsUtils.js';\n\n/**\n * RewardsIntegrationService\n *\n * Handles rewards-related operations and fee discount calculations.\n * Stateless service that coordinates with RewardsController and NetworkController.\n *\n * Instance-based service with constructor injection of platform dependencies.\n */\nexport class RewardsIntegrationService {\n readonly #deps: PerpsPlatformDependencies;\n\n readonly #messenger: PerpsControllerMessengerBase;\n\n /**\n * Create a new RewardsIntegrationService instance\n *\n * @param deps - Platform dependencies for logging, metrics, etc.\n * @param messenger - Controller messenger for cross-controller communication.\n */\n constructor(\n deps: PerpsPlatformDependencies,\n messenger: PerpsControllerMessengerBase,\n ) {\n this.#deps = deps;\n this.#messenger = messenger;\n }\n\n /**\n * Get chain ID for a network client via DI network controller\n *\n * @param networkClientId - The network client identifier to look up.\n * @returns The chain ID string, or undefined if the network client is not found.\n */\n #getChainIdForNetwork(networkClientId: string): string | undefined {\n try {\n const networkClient = this.#messenger.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n return networkClient.configuration.chainId;\n } catch {\n // Network client may not exist\n return undefined;\n }\n }\n\n /**\n * Calculate user fee discount from rewards\n * Returns discount in basis points (e.g., 6500 = 65% discount)\n *\n * @returns The fee discount in basis points, or undefined if unavailable.\n */\n async calculateUserFeeDiscount(): Promise<number | undefined> {\n try {\n const evmAccount = getSelectedEvmAccountFromMessenger(this.#messenger);\n\n if (!evmAccount) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: No EVM account found for fee discount',\n );\n return undefined;\n }\n\n // Get the chain ID via DI network controller\n const networkState = this.#messenger.call('NetworkController:getState');\n const { selectedNetworkClientId } = networkState;\n const chainId = this.#getChainIdForNetwork(selectedNetworkClientId);\n\n if (!chainId) {\n this.#deps.logger.error(\n new Error('Chain ID not found for fee discount calculation'),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {\n selectedNetworkClientId,\n },\n },\n },\n );\n return undefined;\n }\n\n // Use pure utility function for CAIP formatting (pass logger for error reporting)\n const caipAccountId = formatAccountToCaipAccountId(\n evmAccount.address,\n chainId,\n this.#deps.logger,\n );\n\n if (!caipAccountId) {\n this.#deps.logger.error(\n new Error('Failed to format CAIP account ID for fee discount'),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {\n address: evmAccount.address,\n chainId,\n selectedNetworkClientId,\n },\n },\n },\n );\n return undefined;\n }\n\n // Use rewards via DI (no RewardsController in Core yet).\n // The rewards controller needs the perps MetaMask builder base fee in\n // bips to convert an absolute VIP fee into a discount fraction.\n const discountBips = await this.#deps.rewards.getPerpsDiscountForAccount(\n caipAccountId,\n BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR,\n );\n\n // null = subscription state not hydrated yet; surface as undefined so\n // callers don't treat it as a definitive \"no discount\" answer.\n if (discountBips === null) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount unavailable (subscription state not hydrated)',\n { address: evmAccount.address, caipAccountId },\n );\n return undefined;\n }\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount calculated',\n {\n address: evmAccount.address,\n caipAccountId,\n discountBips,\n discountPercentage: discountBips / 100,\n },\n );\n\n return discountBips;\n } catch (error) {\n this.#deps.logger.error(\n ensureError(\n error,\n 'RewardsIntegrationService.calculateUserFeeDiscount',\n ),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {},\n },\n },\n );\n return undefined;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"RewardsIntegrationService.mjs","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BASIS_POINTS_DIVISOR,\n BUILDER_FEE_CONFIG,\n} from '../constants/hyperLiquidConfig.js';\nimport {\n PERPS_CONSTANTS,\n SUBSCRIPTION_BENEFITS_CACHE,\n} from '../constants/perpsConfig.js';\nimport type {\n PerpsFeeResolution,\n PerpsFeeSource,\n PerpsPlatformDependencies,\n PerpsSubscriptionBenefits,\n PerpsSubscriptionFeeWaiverStatus,\n} from '../types/index.js';\nimport type { PerpsControllerMessengerBase } from '../types/messenger.js';\nimport { getSelectedEvmAccountFromMessenger } from '../utils/accountUtils.js';\nimport { ensureError } from '../utils/errorUtils.js';\nimport { formatAccountToCaipAccountId } from '../utils/rewardsUtils.js';\n\n/**\n * Default MetaMask builder fee, in basis points.\n * This is the fee every user pays when no cheaper source applies.\n */\nconst DEFAULT_FEE_BIPS =\n BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR;\n\n/**\n * Cached subscription benefits plus the time they were read.\n */\ntype BenefitsSnapshot = {\n benefits: PerpsSubscriptionBenefits | null;\n fetchedAt: number;\n};\n\n/**\n * RewardsIntegrationService\n *\n * Owns the unified perps fee resolver: it considers every fee source and\n * returns the lowest fee, expressed as the discount bips providers consume.\n *\n * Sources, all in fee basis points (lowest wins):\n * - `default` — {@link BUILDER_FEE_CONFIG}, the fee with no reductions.\n * - `rewards` — VIP and season, collapsed into one discount by\n * `RewardsController` (`rewards.getPerpsDiscountForAccount`), so this service\n * does not re-derive the VIP/season split.\n * - `subscription` — `0` bips, but only when the eligibility gate passes on a\n * cached read of the profile's benefits.\n *\n * On a tie the cheaper-to-explain source wins, in the order\n * `subscription` > `rewards` > `default`.\n *\n * The benefits cache is stale-while-revalidate: fee resolution is a pure read\n * of the cached snapshot, while preview and lifecycle callers refresh it\n * explicitly. Nothing is reserved or committed client-side, so backend\n * exhaustion needs no release logic — the next refresh simply stops passing\n * the gate.\n *\n * Instance-based service with constructor injection of platform dependencies.\n */\nexport class RewardsIntegrationService {\n readonly #deps: PerpsPlatformDependencies;\n\n readonly #messenger: PerpsControllerMessengerBase;\n\n /** Last successful benefits read, or undefined before the first one. */\n #benefitsSnapshot: BenefitsSnapshot | undefined;\n\n /**\n * When the last benefits read finished, successful or not.\n *\n * Separate from `#benefitsSnapshot.fetchedAt`, which only advances on\n * success: a failing read must still throttle the next preview refresh,\n * otherwise an outage turns every fee preview into a new request.\n */\n #lastAttemptAt: number | undefined;\n\n /** In-flight refresh, deduped so only one runs at a time. */\n #benefitsRefresh: Promise<void> | undefined;\n\n /**\n * Identity generation for the cached benefits.\n *\n * Bumped by {@link invalidateSubscriptionBenefits}; a read that resolves\n * against a superseded epoch is discarded rather than written back, so a\n * refresh issued for the previous profile cannot repopulate the cache after\n * a sign-out or profile switch.\n */\n #benefitsEpoch = 0;\n\n /**\n * Create a new RewardsIntegrationService instance\n *\n * @param deps - Platform dependencies for logging, metrics, etc.\n * @param messenger - Controller messenger for cross-controller communication.\n */\n constructor(\n deps: PerpsPlatformDependencies,\n messenger: PerpsControllerMessengerBase,\n ) {\n this.#deps = deps;\n this.#messenger = messenger;\n }\n\n /**\n * Get chain ID for a network client via DI network controller\n *\n * @param networkClientId - The network client identifier to look up.\n * @returns The chain ID string, or undefined if the network client is not found.\n */\n #getChainIdForNetwork(networkClientId: string): string | undefined {\n try {\n const networkClient = this.#messenger.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n return networkClient.configuration.chainId;\n } catch {\n // Network client may not exist\n return undefined;\n }\n }\n\n /**\n * Calculate user fee discount from the unified fee resolver.\n * Returns discount in basis points (e.g., 6500 = 65% discount)\n *\n * @returns The fee discount in basis points, or undefined if no source resolved.\n */\n async calculateUserFeeDiscount(): Promise<number | undefined> {\n const resolution = await this.resolveFee();\n return resolution.discountBips;\n }\n\n /**\n * Resolve the MetaMask builder fee across every source and return the lowest.\n *\n * Never throws and never starts a subscription benefits read: a failing or\n * unresolved cached source simply drops out of the comparison, so the worst\n * case is the default fee rather than an error or an over-granted waiver.\n *\n * @returns The winning fee, its source, and the subscription gate outcome.\n */\n async resolveFee(): Promise<PerpsFeeResolution> {\n const rewardsDiscountBips = await this.#calculateRewardsDiscount();\n // Pure cache read: subscription benefits must never start a network request\n // while an order is being prepared for signing.\n const subscription = this.getSubscriptionFeeWaiverStatus();\n\n let feeBips = DEFAULT_FEE_BIPS;\n let source: PerpsFeeSource = 'default';\n\n if (rewardsDiscountBips !== undefined) {\n const rewardsFeeBips =\n DEFAULT_FEE_BIPS * (1 - rewardsDiscountBips / BASIS_POINTS_DIVISOR);\n // `<=` so an equal rewards fee still reports the rewards source, keeping\n // a resolved 0% discount distinguishable from an unresolved one.\n if (rewardsFeeBips <= feeBips) {\n feeBips = rewardsFeeBips;\n source = 'rewards';\n }\n }\n\n // Nothing can undercut a waived fee, so the gate passing always wins.\n if (subscription.eligible) {\n feeBips = 0;\n source = 'subscription';\n }\n\n const discountBips =\n source === 'default'\n ? undefined\n : Math.round((1 - feeBips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR);\n\n this.#deps.debugLogger.log('RewardsIntegrationService: Fee resolved', {\n source,\n feeBips,\n discountBips,\n defaultFeeBips: DEFAULT_FEE_BIPS,\n rewardsDiscountBips,\n subscriptionEligible: subscription.eligible,\n subscriptionReason: subscription.reason,\n });\n\n return { feeBips, discountBips, source, subscription };\n }\n\n /**\n * Read the subscription fee-waiver gate from the cached benefits snapshot.\n *\n * Synchronous and side-effect free. The returned value always comes from\n * what is already cached; preview and lifecycle callers own hydration.\n *\n * @returns Whether the waiver applies, why, and the remaining notional.\n */\n getSubscriptionFeeWaiverStatus(): PerpsSubscriptionFeeWaiverStatus {\n if (!this.#deps.subscription) {\n return { eligible: false, reason: 'no-source' };\n }\n\n const now = Date.now();\n const snapshot = this.#benefitsSnapshot;\n const age = snapshot ? now - snapshot.fetchedAt : Infinity;\n if (!snapshot) {\n return { eligible: false, reason: 'not-hydrated' };\n }\n\n if (age > SUBSCRIPTION_BENEFITS_CACHE.MaxStaleMs) {\n // Past the ceiling we cannot tell whether the cap is still available, so\n // fall back to the next-lowest source rather than over-granting.\n return { eligible: false, reason: 'stale' };\n }\n\n return evaluateFeeWaiverGate(snapshot.benefits);\n }\n\n /**\n * Refresh the cached subscription benefits snapshot.\n *\n * Deduped: concurrent callers share the in-flight request. Rejections are\n * logged and swallowed, leaving the previous snapshot in place. Preview and\n * lifecycle callers invoke this outside order submission.\n *\n * @returns A promise that settles when the refresh completes.\n */\n async refreshSubscriptionBenefits(): Promise<void> {\n const source = this.#deps.subscription;\n if (!source) {\n return;\n }\n\n if (this.#benefitsRefresh) {\n await this.#benefitsRefresh;\n return;\n }\n\n const now = Date.now();\n const snapshotAge = this.#benefitsSnapshot\n ? now - this.#benefitsSnapshot.fetchedAt\n : Infinity;\n const sinceAttempt =\n this.#lastAttemptAt === undefined ? Infinity : now - this.#lastAttemptAt;\n if (\n snapshotAge < SUBSCRIPTION_BENEFITS_CACHE.FreshMs ||\n sinceAttempt < SUBSCRIPTION_BENEFITS_CACHE.FreshMs\n ) {\n return;\n }\n\n const refresh = this.#readSubscriptionBenefits(source);\n this.#benefitsRefresh = refresh;\n // `finally` always defers, so this never clears the handle we just set.\n refresh\n .finally(() => {\n if (this.#benefitsRefresh === refresh) {\n this.#benefitsRefresh = undefined;\n }\n })\n .catch(() => undefined);\n\n await refresh;\n }\n\n /**\n * Drop the cached benefits snapshot.\n *\n * Call this when the identity behind the benefits changes — sign-out, or a\n * profile switch — since the snapshot carries no profile identity of its own\n * and would otherwise keep answering for the previous profile until the next\n * successful refresh. The next status read reports `not-hydrated`, so the\n * waiver is withheld until a preview or lifecycle caller hydrates it.\n */\n invalidateSubscriptionBenefits(): void {\n this.#benefitsSnapshot = undefined;\n this.#lastAttemptAt = undefined;\n // Fence any in-flight read: it was issued for the previous identity, so its\n // result must not repopulate the cache after this point.\n this.#benefitsEpoch += 1;\n // Drop the dedupe handle too. The fenced read can only be discarded, so\n // leaving it in place would make the next refresh await it instead of\n // fetching for the new identity. Its `finally` guard compares against the\n // current handle, so it will not clear whatever replaces it here.\n this.#benefitsRefresh = undefined;\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Subscription benefits cache invalidated',\n );\n }\n\n /**\n * Perform one benefits read and store it, keeping the previous snapshot on\n * error. Never rejects, so callers cannot produce an unhandled rejection.\n *\n * @param source - The injected subscription benefits source.\n */\n async #readSubscriptionBenefits(\n source: NonNullable<PerpsPlatformDependencies['subscription']>,\n ): Promise<void> {\n const epoch = this.#benefitsEpoch;\n\n try {\n const benefits = await source.getPerpsBenefits();\n\n if (epoch !== this.#benefitsEpoch) {\n // Invalidated while this read was in flight: it belongs to a previous\n // identity, so discarding it is the only safe outcome.\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Discarding benefits read from a previous identity',\n );\n return;\n }\n\n this.#benefitsSnapshot = { benefits, fetchedAt: Date.now() };\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Subscription benefits refreshed',\n {\n status: benefits?.status,\n entitled: benefits?.perpsFeeWaiver?.entitled,\n usage: benefits?.perpsFeeWaiver?.usage,\n exhausted: benefits?.perpsFeeWaiver?.exhausted,\n },\n );\n } catch (error) {\n // Keep the previous snapshot: an unreachable benefits endpoint must not\n // erase a valid cache, and it must never grant the waiver either.\n this.#deps.logger.error(\n ensureError(\n error,\n 'RewardsIntegrationService.refreshSubscriptionBenefits',\n ),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.refreshSubscriptionBenefits',\n data: {},\n },\n },\n );\n } finally {\n // Recorded on failure too — this is what throttles the retry loop. Not\n // recorded for a fenced read: that attempt belongs to a previous\n // identity, and letting it throttle would delay the new identity's first\n // fetch by a whole freshness window.\n if (epoch === this.#benefitsEpoch) {\n this.#lastAttemptAt = Date.now();\n }\n }\n }\n\n /**\n * Resolve the rewards (VIP + season) discount for the selected account.\n *\n * @returns The discount in basis points, or undefined when unavailable.\n */\n async #calculateRewardsDiscount(): Promise<number | undefined> {\n try {\n const evmAccount = getSelectedEvmAccountFromMessenger(this.#messenger);\n\n if (!evmAccount) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: No EVM account found for fee discount',\n );\n return undefined;\n }\n\n // Get the chain ID via DI network controller\n const networkState = this.#messenger.call('NetworkController:getState');\n const { selectedNetworkClientId } = networkState;\n const chainId = this.#getChainIdForNetwork(selectedNetworkClientId);\n\n if (!chainId) {\n this.#deps.logger.error(\n new Error('Chain ID not found for fee discount calculation'),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {\n selectedNetworkClientId,\n },\n },\n },\n );\n return undefined;\n }\n\n // Use pure utility function for CAIP formatting (pass logger for error reporting)\n const caipAccountId = formatAccountToCaipAccountId(\n evmAccount.address,\n chainId,\n this.#deps.logger,\n );\n\n if (!caipAccountId) {\n this.#deps.logger.error(\n new Error('Failed to format CAIP account ID for fee discount'),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {\n address: evmAccount.address,\n chainId,\n selectedNetworkClientId,\n },\n },\n },\n );\n return undefined;\n }\n\n // Use rewards via DI (no RewardsController in Core yet).\n // The rewards controller needs the perps MetaMask builder base fee in\n // bips to convert an absolute VIP fee into a discount fraction.\n const discountBips = await this.#deps.rewards.getPerpsDiscountForAccount(\n caipAccountId,\n DEFAULT_FEE_BIPS,\n );\n\n // null = subscription state not hydrated yet; surface as undefined so\n // callers don't treat it as a definitive \"no discount\" answer.\n if (discountBips === null) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount unavailable (subscription state not hydrated)',\n { address: evmAccount.address, caipAccountId },\n );\n return undefined;\n }\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount calculated',\n {\n address: evmAccount.address,\n caipAccountId,\n discountBips,\n discountPercentage: discountBips / 100,\n },\n );\n\n return discountBips;\n } catch (error) {\n this.#deps.logger.error(\n ensureError(\n error,\n 'RewardsIntegrationService.calculateUserFeeDiscount',\n ),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.calculateUserFeeDiscount',\n data: {},\n },\n },\n );\n return undefined;\n }\n }\n}\n\n/**\n * Evaluate the perps fee-waiver eligibility gate against a benefits snapshot.\n *\n * The gate is `status=active` AND `perpsFeeWaiver` entitled AND\n * `usage=available`. A backend `exhausted` flag (or an `exhausted` usage) fails\n * the gate on its own; anything short of an affirmative `available` is treated\n * as not entitled, because the waiver is only granted on positive evidence.\n * A `null` payload means there is no subscription at all, which is reported\n * separately from a subscription that exists but is not active.\n *\n * @param benefits - The cached benefits payload, or null when there is none.\n * @returns The gate outcome plus the remaining notional when reported.\n */\nfunction evaluateFeeWaiverGate(\n benefits: PerpsSubscriptionBenefits | null,\n): PerpsSubscriptionFeeWaiverStatus {\n const waiver = benefits?.perpsFeeWaiver;\n const { remainingNotionalUsd } = waiver ?? {};\n\n // `null` is the DI contract's \"nothing to report\" (signed out, no profile),\n // which is distinct from a subscription that exists but is not active.\n if (benefits === null) {\n return { eligible: false, reason: 'no-subscription' };\n }\n\n if (benefits.status !== 'active') {\n return { eligible: false, reason: 'inactive', remainingNotionalUsd };\n }\n\n if (waiver?.entitled !== true) {\n return { eligible: false, reason: 'not-entitled', remainingNotionalUsd };\n }\n\n if (waiver.exhausted === true || waiver.usage === 'exhausted') {\n return { eligible: false, reason: 'exhausted', remainingNotionalUsd };\n }\n\n if (waiver.usage !== 'available') {\n return { eligible: false, reason: 'not-entitled', remainingNotionalUsd };\n }\n\n return { eligible: true, reason: 'eligible', remainingNotionalUsd };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceContext.cjs","sourceRoot":"","sources":["../../src/services/ServiceContext.ts"],"names":[],"mappings":"","sourcesContent":["import type { PerpsControllerState } from '../PerpsController.js';\nimport type { Order, Position } from '../types/index.js';\n\n/**\n * ServiceContext\n *\n * Lightweight per-call context for Perps services.\n * Contains ONLY data that varies per operation:\n * - Tracing context (provider, network)\n * - Error context (controller, method)\n * - State management callbacks\n * - Query/action callbacks specific to the operation\n *\n * Platform dependencies (logging, metrics, tracing) are injected into service\n * instances via constructor, not passed per-call.\n *\n * Controller-level singletons (RewardsController, NetworkController, messenger)\n * are also injected into services that need them, not passed per-call.\n *\n * This enables:\n * - Clean method signatures (no verbose dependency passing)\n * - Fat services with complete orchestration\n * - Thin controller with pure delegation\n * - Easy testing through mock contexts and constructor injection\n */\nexport type ServiceContext = {\n /**\n * Tracing context for performance monitoring\n * Used in trace() calls to tag operations\n */\n tracingContext: {\n provider: string;\n isTestnet: boolean;\n };\n\n /**\n * Error logging context\n * Provides consistent error logging across services\n */\n errorContext: {\n controller: string;\n method: string;\n extra?: Record<string, unknown>;\n };\n\n /**\n * State management functions (optional)\n * Only provided for operations that need to mutate controller state\n * Example: Trading operations that update lastTransaction\n */\n stateManager?: {\n update: (updater: (state: PerpsControllerState) => void) => void;\n getState: () => PerpsControllerState;\n };\n\n /**\n * Query functions for dependent data\n * Required by: Operations that need to fetch related data\n */\n getOpenOrders?: () => Promise<Order[]>;\n getPositions?: () => Promise<Position[]>;\n\n /**\n * Callback functions for controller-specific operations\n */\n saveTradeConfiguration?: (symbol: string, leverage: number) => void;\n\n /**\n * Feature flag configuration callbacks\n * Required by: FeatureFlagConfigurationService\n */\n getBlockedRegionList?: () => {\n list: string[];\n source: 'remote' | 'fallback';\n };\n setBlockedRegionList?: (\n list: string[],\n source: 'remote' | 'fallback',\n ) => void;\n getHip3Config?: () => {\n enabled: boolean;\n allowlistMarkets: string[];\n blocklistMarkets: string[];\n source: 'remote' | 'fallback';\n };\n setHip3Config?: (config: {\n enabled?: boolean;\n allowlistMarkets?: string[];\n blocklistMarkets?: string[];\n source: 'remote' | 'fallback';\n }) => void;\n incrementHip3ConfigVersion?: () => number;\n refreshEligibility?: () => Promise<void>;\n};\n"]}
1
+ {"version":3,"file":"ServiceContext.cjs","sourceRoot":"","sources":["../../src/services/ServiceContext.ts"],"names":[],"mappings":"","sourcesContent":["import type { PerpsControllerState } from '../PerpsController.js';\nimport type {\n Order,\n PerpsGlobalSnapshotRequest,\n PerpsSubscriptionFeeWaiverStatus,\n Position,\n} from '../types/index.js';\n\n/**\n * ServiceContext\n *\n * Lightweight per-call context for Perps services.\n * Contains ONLY data that varies per operation:\n * - Tracing context (provider, network)\n * - Error context (controller, method)\n * - State management callbacks\n * - Query/action callbacks specific to the operation\n *\n * Platform dependencies (logging, metrics, tracing) are injected into service\n * instances via constructor, not passed per-call.\n *\n * Controller-level singletons (RewardsController, NetworkController, messenger)\n * are also injected into services that need them, not passed per-call.\n *\n * This enables:\n * - Clean method signatures (no verbose dependency passing)\n * - Fat services with complete orchestration\n * - Thin controller with pure delegation\n * - Easy testing through mock contexts and constructor injection\n */\nexport type ServiceContext = {\n /**\n * Tracing context for performance monitoring\n * Used in trace() calls to tag operations\n */\n tracingContext: {\n provider: string;\n isTestnet: boolean;\n };\n\n /**\n * Error logging context\n * Provides consistent error logging across services\n */\n errorContext: {\n controller: string;\n method: string;\n extra?: Record<string, unknown>;\n };\n\n /**\n * State management functions (optional)\n * Only provided for operations that need to mutate controller state\n * Example: Trading operations that update lastTransaction\n */\n stateManager?: {\n update: (updater: (state: PerpsControllerState) => void) => void;\n getState: () => PerpsControllerState;\n };\n\n /**\n * Query functions for dependent data\n * Required by: Operations that need to fetch related data\n */\n getOpenOrders?: () => Promise<Order[]>;\n getPositions?: () => Promise<Position[]>;\n\n /**\n * Exact per-call identity and guards for adopting a global market snapshot.\n * Omitted when the active provider or DEX configuration is not static.\n */\n globalSnapshot?: {\n request: PerpsGlobalSnapshotRequest;\n isCurrent: () => boolean;\n isMarketAllowed: (symbol: string) => boolean;\n };\n\n /**\n * Cached subscription fee-waiver status for read-only fee previews.\n * Read by the controller from `RewardsIntegrationService` — the same cached\n * benefits snapshot the fee resolver uses — and omitted entirely when no\n * subscription source is wired.\n */\n subscriptionFeeWaiver?: PerpsSubscriptionFeeWaiverStatus;\n\n /**\n * Callback functions for controller-specific operations\n */\n saveTradeConfiguration?: (symbol: string, leverage: number) => void;\n\n /**\n * Feature flag configuration callbacks\n * Required by: FeatureFlagConfigurationService\n */\n getBlockedRegionList?: () => {\n list: string[];\n source: 'remote' | 'fallback';\n };\n setBlockedRegionList?: (\n list: string[],\n source: 'remote' | 'fallback',\n ) => void;\n getHip3Config?: () => {\n enabled: boolean;\n allowlistMarkets: string[];\n blocklistMarkets: string[];\n source: 'remote' | 'fallback';\n };\n setHip3Config?: (config: {\n enabled?: boolean;\n allowlistMarkets?: string[];\n blocklistMarkets?: string[];\n source: 'remote' | 'fallback';\n }) => void;\n incrementHip3ConfigVersion?: () => number;\n refreshEligibility?: () => Promise<void>;\n};\n"]}