@metamask-previews/perps-controller 20.1.0-preview-b4b0d7a09 → 21.0.0-preview-55fcf3ef7

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.
@@ -10,6 +10,24 @@ import { isMissingActionHandlerError } from './missingActionHandler.js';
10
10
  * This is the fee every user pays when no cheaper source applies.
11
11
  */
12
12
  const DEFAULT_FEE_BIPS = BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR;
13
+ /** Number of venue fee increments in one basis point. */
14
+ const BUILDER_FEE_TENTHS_PER_BIP = BUILDER_FEE_CONFIG.MaxFeeTenthsBps / DEFAULT_FEE_BIPS;
15
+ /**
16
+ * Floor an absolute fee to the venue's integer tenths without dropping an
17
+ * exact boundary because of multiplication error.
18
+ *
19
+ * The two-ULP tolerance is far smaller than a venue increment, but covers
20
+ * values such as `0.29999999999999993` produced by ordinary decimal
21
+ * arithmetic when the intended value is 0.3.
22
+ *
23
+ * @param feeBips - Absolute builder fee in basis points.
24
+ * @returns The chargeable fee in integer tenths of a basis point.
25
+ */
26
+ function quantizeAbsoluteFeeTenthsBps(feeBips) {
27
+ const scaledFee = feeBips * BUILDER_FEE_TENTHS_PER_BIP;
28
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(scaledFee)) * 2;
29
+ return Math.floor(scaledFee + tolerance);
30
+ }
13
31
  /** `SubscriptionController` reports allowances in micro-USD. */
14
32
  const MICRO_USD_PER_USD = 1_000_000;
15
33
  /**
@@ -20,14 +38,15 @@ const MICRO_USD_PER_USD = 1_000_000;
20
38
  *
21
39
  * Sources, all in fee basis points (lowest wins):
22
40
  * - `default` — {@link BUILDER_FEE_CONFIG}, the fee with no reductions.
23
- * - `rewards` — VIP and season, collapsed into one discount by
24
- * `RewardsController` (`rewards.getPerpsDiscountForAccount`), so this service
25
- * does not re-derive the VIP/season split.
41
+ * - `rewards` — the account-scoped VIP and season discount returned by
42
+ * `rewards.getPerpsDiscountForAccount`.
43
+ * - `grant` — an independent, expiring, route-scoped fee returned by
44
+ * `rewards.getPerpsTradingFeeGrant`.
26
45
  * - `subscription` — `0` bips, but only when the eligibility gate passes on a
27
46
  * cached read of the profile's benefits.
28
47
  *
29
- * On a tie the cheaper-to-explain source wins, in the order
30
- * `subscription` > `rewards` > `default`.
48
+ * Rewards wins ties with default. Grant and subscription must be strictly
49
+ * cheaper after venue quantization to win.
31
50
  *
32
51
  * The benefits cache is stale-while-revalidate: fee resolution is a pure read
33
52
  * of the cached snapshot, while preview and lifecycle callers refresh it
@@ -89,10 +108,12 @@ export class RewardsIntegrationService {
89
108
  * Returns discount in basis points (e.g., 6500 = 65% discount)
90
109
  *
91
110
  * @param orderNotionalUsd - Order notional (USD), when the caller knows it.
111
+ * @param scope - Provider route used to request route-scoped fee sources.
112
+ * Omission intentionally excludes grants for compatibility callers.
92
113
  * @returns The fee discount in basis points, or undefined if no source resolved.
93
114
  */
94
- async calculateUserFeeDiscount(orderNotionalUsd) {
95
- const resolution = await this.resolveFee(orderNotionalUsd);
115
+ async calculateUserFeeDiscount(orderNotionalUsd, scope) {
116
+ const resolution = await this.resolveFee(orderNotionalUsd, scope);
96
117
  return resolution.discountBips;
97
118
  }
98
119
  /**
@@ -115,17 +136,32 @@ export class RewardsIntegrationService {
115
136
  * source instead.
116
137
  *
117
138
  * @param orderNotionalUsd - Order notional (USD), when the caller knows it.
139
+ * @param scope - Provider route used to request route-scoped fee sources.
140
+ * Omission intentionally excludes grants.
118
141
  * @returns The winning fee, its source, and the subscription gate outcome.
119
142
  */
120
- async resolveFee(orderNotionalUsd) {
121
- const rewardsDiscountBips = await this.#calculateRewardsDiscount();
143
+ async resolveFee(orderNotionalUsd, scope) {
144
+ const [rewardsDiscount, grantCandidate] = await Promise.all([
145
+ this.#calculateRewardsDiscount(),
146
+ scope
147
+ ? this.#getPerpsTradingFeeGrant(scope)
148
+ : Promise.resolve(null),
149
+ ]);
150
+ // Expiry is deliberately checked only after every concurrent candidate has
151
+ // settled. A grant that expires while the account-scoped rewards request is
152
+ // in flight must not be selected.
153
+ const grant = scope && isValidTradingFeeGrant(grantCandidate, scope, Date.now())
154
+ ? grantCandidate
155
+ : undefined;
122
156
  // Pure cache read: subscription benefits must never start a network request
123
157
  // while an order is being prepared for signing.
124
158
  const subscription = this.getSubscriptionFeeWaiverStatus();
125
159
  let feeBips = DEFAULT_FEE_BIPS;
126
160
  let source = 'default';
127
- if (rewardsDiscountBips !== undefined) {
128
- const rewardsFeeBips = DEFAULT_FEE_BIPS * (1 - rewardsDiscountBips / BASIS_POINTS_DIVISOR);
161
+ const toTenthsBps = (bips) => quantizeBuilderFeeTenthsBps(Math.round((1 - bips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR));
162
+ const fromTenthsBps = (tenthsBps) => tenthsBps / BUILDER_FEE_TENTHS_PER_BIP;
163
+ if (rewardsDiscount !== undefined) {
164
+ const rewardsFeeBips = DEFAULT_FEE_BIPS * (1 - rewardsDiscount / BASIS_POINTS_DIVISOR);
129
165
  // `<=` so an equal rewards fee still reports the rewards source, keeping
130
166
  // a resolved 0% discount distinguishable from an unresolved one.
131
167
  if (rewardsFeeBips <= feeBips) {
@@ -133,6 +169,21 @@ export class RewardsIntegrationService {
133
169
  source = 'rewards';
134
170
  }
135
171
  }
172
+ // A grant only wins when it lowers the fee the venue will actually charge.
173
+ // Rewards therefore keeps a quantized tie, and default keeps a grant that
174
+ // does not reduce the on-wire rate.
175
+ // Grants are absolute fees and may carry more precision than the legacy
176
+ // integer discount. Quantize that absolute candidate first: converting a
177
+ // 0.0999-bip grant to a discount first would round it to 9900 bips and
178
+ // submit 0.1 bip instead of the venue-floored zero.
179
+ const grantFeeTenthsBps = grant
180
+ ? quantizeAbsoluteFeeTenthsBps(grant.feeBips)
181
+ : undefined;
182
+ if (grantFeeTenthsBps !== undefined &&
183
+ grantFeeTenthsBps < toTenthsBps(feeBips)) {
184
+ feeBips = fromTenthsBps(grantFeeTenthsBps);
185
+ source = 'grant';
186
+ }
136
187
  const waiver = resolveSubscriptionWaiverRate({
137
188
  status: subscription,
138
189
  maxFeeBips: DEFAULT_FEE_BIPS,
@@ -140,11 +191,8 @@ export class RewardsIntegrationService {
140
191
  });
141
192
  let subscriptionWaiverKind;
142
193
  let subscriptionCoveredNotionalUsd;
143
- // The waiver competes like any other source. A full waiver still wins on
144
- // `<=`, but a partial blend only wins when it is genuinely cheaper than the
145
- // rewards discount — the ADR's requirement that subscription be able to
146
- // lose.
147
- //
194
+ // Full and partial waivers compete with the rewards fee. Subscription
195
+ // only wins when it produces a genuinely cheaper fee on the wire.
148
196
  // Compared *after* venue quantization, because that is the fee the order
149
197
  // pays. The builder fee is submitted in integer tenths of a basis point, so
150
198
  // a blend like 9.9999 bips is cheaper than the 10-bip default in arithmetic
@@ -152,7 +200,6 @@ export class RewardsIntegrationService {
152
200
  // would label such an order `source: 'subscription'` with a 0-bip discount
153
201
  // while it pays full price, and would disagree with the cloid marker, which
154
202
  // already gates on the quantized fee.
155
- const toTenthsBps = (bips) => quantizeBuilderFeeTenthsBps(Math.round((1 - bips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR));
156
203
  // Subscription must be strictly cheaper on the wire to claim the order. A
157
204
  // tie buys the user nothing — the same rate is already available from the
158
205
  // source that won — while claiming it marks the cloid and spends the
@@ -175,7 +222,9 @@ export class RewardsIntegrationService {
175
222
  feeBips,
176
223
  discountBips,
177
224
  defaultFeeBips: DEFAULT_FEE_BIPS,
178
- rewardsDiscountBips,
225
+ rewardsDiscountBips: rewardsDiscount,
226
+ grantFeeBips: grant?.feeBips,
227
+ grantExpiresAt: grant?.expiresAt,
179
228
  orderNotionalUsd,
180
229
  subscriptionEligible: subscription.eligible,
181
230
  subscriptionReason: subscription.reason,
@@ -570,9 +619,9 @@ export class RewardsIntegrationService {
570
619
  this.#deps.debugLogger.log('RewardsIntegrationService: Fee discount unavailable (NetworkController not provided)', { error: ensureError(error).message });
571
620
  }
572
621
  /**
573
- * Resolve the rewards (VIP + season) discount for the selected account.
622
+ * Resolve the VIP and season discount for the selected account.
574
623
  *
575
- * @returns The discount in basis points, or undefined when unavailable.
624
+ * @returns The numeric discount, or undefined when unavailable.
576
625
  */
577
626
  async #calculateRewardsDiscount() {
578
627
  try {
@@ -668,6 +717,47 @@ export class RewardsIntegrationService {
668
717
  return undefined;
669
718
  }
670
719
  }
720
+ /**
721
+ * Read the independent trading-fee grant for a provider route.
722
+ *
723
+ * @param scope - Exact route requested by the resolver.
724
+ * @returns The candidate, or null when unsupported or unavailable.
725
+ */
726
+ async #getPerpsTradingFeeGrant(scope) {
727
+ if (!this.#deps.rewards.getPerpsTradingFeeGrant) {
728
+ return null;
729
+ }
730
+ try {
731
+ return await this.#deps.rewards.getPerpsTradingFeeGrant(scope);
732
+ }
733
+ catch (error) {
734
+ this.#deps.logger.error(ensureError(error, 'RewardsIntegrationService.getPerpsTradingFeeGrant'), {
735
+ tags: { feature: PERPS_CONSTANTS.FeatureName },
736
+ context: {
737
+ name: 'RewardsIntegrationService.getPerpsTradingFeeGrant',
738
+ data: {},
739
+ },
740
+ });
741
+ return null;
742
+ }
743
+ }
744
+ }
745
+ /**
746
+ * Validate a trading-fee grant at the time fee selection occurs.
747
+ *
748
+ * @param grant - Candidate returned by the client-owned RewardsController.
749
+ * @param scope - Exact provider route requested by the resolver.
750
+ * @param now - Selection time as Unix milliseconds.
751
+ * @returns Whether the candidate is valid, unexpired, and matches the route.
752
+ */
753
+ function isValidTradingFeeGrant(grant, scope, now) {
754
+ return Boolean(grant &&
755
+ grant.providerId === scope.providerId &&
756
+ grant.isTestnet === scope.isTestnet &&
757
+ Number.isFinite(grant.feeBips) &&
758
+ grant.feeBips >= 0 &&
759
+ Number.isFinite(grant.expiresAt) &&
760
+ now < grant.expiresAt);
671
761
  }
672
762
  /**
673
763
  * Whether a benefits rejection definitively means "this profile is not
@@ -1 +1 @@
1
- {"version":3,"file":"RewardsIntegrationService.js","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SubscriptionBenefitsResponse } from '@metamask/subscription-controller';\n\nimport {\n BASIS_POINTS_DIVISOR,\n BUILDER_FEE_CONFIG,\n HYPERLIQUID_MAINNET_CHAIN_ID,\n HYPERLIQUID_TESTNET_CHAIN_ID,\n} from '../constants/hyperLiquidConfig.js';\nimport {\n PERPS_CONSTANTS,\n SUBSCRIPTION_BENEFITS_CACHE,\n SUBSCRIPTION_FEE_WAIVER_FLAG,\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';\nimport {\n quantizeBuilderFeeTenthsBps,\n resolveSubscriptionWaiverRate,\n} from '../utils/subscriptionFeeWaiver.js';\nimport { isMissingActionHandlerError } from './missingActionHandler.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/** `SubscriptionController` reports allowances in micro-USD. */\nconst MICRO_USD_PER_USD = 1_000_000;\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 * CAIP-10 addresses already registered with the subscription profile this\n * session, so preview does not re-send the same registration on every\n * keystroke. Cleared on account switch and on cache invalidation.\n */\n readonly #registeredTradingAddresses = new Set<string>();\n\n /**\n * Whether `SubscriptionController:getBenefits` has ever answered on this\n * messenger. Action registration can be delegated, in which case it does not\n * appear in `getRegisteredActionTypes`, so an answered call is the only\n * reliable proof that the messenger route is available.\n */\n #messengerBenefitsAnswered = false;\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 * Calculate user fee discount from the unified fee resolver.\n * Returns discount in basis points (e.g., 6500 = 65% discount)\n *\n * @param orderNotionalUsd - Order notional (USD), when the caller knows it.\n * @returns The fee discount in basis points, or undefined if no source resolved.\n */\n async calculateUserFeeDiscount(\n orderNotionalUsd?: number,\n ): Promise<number | undefined> {\n const resolution = await this.resolveFee(orderNotionalUsd);\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 * The subscription source contributes an effective rate rather than a flat\n * zero (ADR 0064): the allowance may cover only part of the order, and the\n * blend that results has to be able to lose to a deeper VIP or season\n * discount. Passing the order notional is what makes that blend possible.\n *\n * Without a notional the outcome depends on whether the backend bounded the\n * allowance. An unbounded allowance still resolves to the full waiver — there\n * is no cap to over-consume. A *bounded* one is withheld entirely rather than\n * quoted as a full waiver, because charging nothing on an order of unknown\n * size would silently spend the cap; such a caller gets the next-lowest\n * source instead.\n *\n * @param orderNotionalUsd - Order notional (USD), when the caller knows it.\n * @returns The winning fee, its source, and the subscription gate outcome.\n */\n async resolveFee(orderNotionalUsd?: number): 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 const waiver = resolveSubscriptionWaiverRate({\n status: subscription,\n maxFeeBips: DEFAULT_FEE_BIPS,\n orderNotionalUsd,\n });\n\n let subscriptionWaiverKind: PerpsFeeResolution['subscriptionWaiverKind'];\n let subscriptionCoveredNotionalUsd: number | undefined;\n\n // The waiver competes like any other source. A full waiver still wins on\n // `<=`, but a partial blend only wins when it is genuinely cheaper than the\n // rewards discount — the ADR's requirement that subscription be able to\n // lose.\n //\n // Compared *after* venue quantization, because that is the fee the order\n // pays. The builder fee is submitted in integer tenths of a basis point, so\n // a blend like 9.9999 bips is cheaper than the 10-bip default in arithmetic\n // and identical to it on the wire. Selecting subscription on the raw number\n // would label such an order `source: 'subscription'` with a 0-bip discount\n // while it pays full price, and would disagree with the cloid marker, which\n // already gates on the quantized fee.\n const toTenthsBps = (bips: number): number =>\n quantizeBuilderFeeTenthsBps(\n Math.round((1 - bips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR),\n );\n\n // Subscription must be strictly cheaper on the wire to claim the order. A\n // tie buys the user nothing — the same rate is already available from the\n // source that won — while claiming it marks the cloid and spends the\n // remaining allowance, so a tie is left to the other source. That holds at\n // any rate, including a rewards discount that already reaches 0 bips.\n const waiverSurvivesQuantization =\n toTenthsBps(waiver.feeBips) < toTenthsBps(feeBips);\n\n if (\n waiver.applies &&\n waiver.feeBips <= feeBips &&\n waiverSurvivesQuantization\n ) {\n feeBips = waiver.feeBips;\n source = 'subscription';\n subscriptionWaiverKind = waiver.kind === 'partial' ? 'partial' : 'full';\n subscriptionCoveredNotionalUsd = waiver.coveredNotionalUsd;\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 orderNotionalUsd,\n subscriptionEligible: subscription.eligible,\n subscriptionReason: subscription.reason,\n subscriptionWaiverKind,\n subscriptionCoveredNotionalUsd,\n });\n\n return {\n feeBips,\n discountBips,\n source,\n subscription,\n subscriptionWaiverKind,\n subscriptionCoveredNotionalUsd,\n };\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.#hasSubscriptionSource()) {\n return { eligible: false, reason: 'no-source' };\n }\n\n // ADR 0064 Milestone 8: the subscription source has to be killable on its\n // own. Reported as `no-source` so a disabled flag is indistinguishable from\n // an unwired client downstream — both mean \"subscription contributes\n // nothing\", and neither touches rewards or the default fee.\n if (!this.#isSubscriptionFeeWaiverEnabled()) {\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 * Whether this client has any way to read subscription benefits.\n *\n * Either wiring counts: a registered `SubscriptionController:getBenefits`\n * action (the ADR 0064 target) or the legacy injected `subscription` callback.\n * Requiring the injected one would make the messenger path unreachable on\n * exactly the configuration it was added for, so the messenger is probed by\n * asking whether an action handler exists rather than by calling it — this\n * runs on the synchronous status read and must not start work.\n *\n * @returns True when benefits can be read by some route.\n */\n #hasSubscriptionSource(): boolean {\n if (this.#deps.subscription || this.#messengerBenefitsAnswered) {\n return true;\n }\n\n try {\n // `getRegisteredActionTypes` reports this messenger's own registrations.\n // A delegated action does not appear there, which is why it is only a\n // positive signal — `refreshSubscriptionBenefits` still attempts the call\n // regardless, and an answer sets `#messengerBenefitsAnswered` above.\n return this.#messenger\n .getRegisteredActionTypes()\n .includes('SubscriptionController:getBenefits');\n } catch {\n return false;\n }\n }\n\n /**\n * Whether the subscription fee-waiver source is enabled remotely.\n *\n * Fails open: an absent flag, a malformed value, or an unreachable flag\n * controller all read as enabled, because silently dropping a benefit the\n * user pays for is worse than serving it one release too long. The kill\n * switch is an explicit `false`.\n *\n * @returns True unless the remote flag explicitly disables the source.\n */\n #isSubscriptionFeeWaiverEnabled(): boolean {\n try {\n const { remoteFeatureFlags } = this.#messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n const flag = remoteFeatureFlags?.[SUBSCRIPTION_FEE_WAIVER_FLAG];\n return flag !== false;\n } catch {\n // No flag controller registered, or the read threw: keep the source.\n return true;\n }\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 // Deliberately not gated on `#hasSubscriptionSource`: a delegated action is\n // invisible to `getRegisteredActionTypes`, so the first call is what proves\n // it is reachable. A client with no source at all reads `null` here, which\n // costs one no-op call per freshness window and leaves the gate closed.\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();\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 // The identity behind the registration changed too, so the new one has to\n // announce itself rather than inherit the previous profile's registration.\n this.#registeredTradingAddresses.clear();\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 async #readSubscriptionBenefits(): Promise<void> {\n const epoch = this.#benefitsEpoch;\n\n try {\n const benefits = await this.#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 * Read subscription benefits, preferring the messenger over the DI callback.\n *\n * ADR 0064 moves hydration onto `SubscriptionController`. The messenger is\n * tried first and the injected `subscription` dependency is only a fallback,\n * so a client that ships `SubscriptionController` without the legacy callback\n * hydrates normally — that configuration is the ADR's target, not an edge\n * case. A client with neither gets `null`, which reads as \"no subscription\".\n *\n * @returns The benefits payload, or null when there is none to report.\n */\n async #getPerpsBenefits(): Promise<PerpsSubscriptionBenefits | null> {\n let pending: Promise<PerpsSubscriptionBenefits | null> | undefined;\n try {\n // Called without awaiting so the fallback stays synchronous when no\n // handler is registered: the DI read must start in the same tick, or a\n // caller that inspects the in-flight state sees an idle service.\n const result = this.#messenger.call('SubscriptionController:getBenefits');\n // `undefined` means nothing handled the action, which is the fallback\n // case rather than an answer.\n if (result !== undefined) {\n this.#messengerBenefitsAnswered = true;\n pending = Promise.resolve(result).then(adaptSubscriptionBenefits);\n }\n } catch (error) {\n // A handler that has answered before exists, so a synchronous throw is a\n // real failure rather than an unregistered action. Treating it as the\n // latter would fall through to `null` and erase a valid cached snapshot,\n // exactly as an asynchronous rejection would.\n if (isNotSubscribedRejection(error)) {\n // Definitive \"not entitled\", even thrown synchronously.\n return null;\n }\n // An unregistered or undelegated action throws a recognisable \"no\n // handler\" error; any other synchronous throw came from a handler that\n // exists and failed, so it must not be mistaken for an absent action and\n // cached as `null` — including on the very first call, before one has\n // ever answered.\n if (\n !isMissingActionHandlerError(\n error,\n 'SubscriptionController:getBenefits',\n ) &&\n !this.#deps.subscription\n ) {\n throw error;\n }\n // Otherwise: missing handler, or a throw with a DI source to fall\n // back to.\n }\n\n const fallback = this.#deps.subscription;\n\n if (pending) {\n try {\n return await pending;\n } catch (error) {\n if (isNotSubscribedRejection(error)) {\n // A definitive answer, not a failed read: the profile is not\n // entitled. Returning `null` replaces the cached snapshot, which is\n // the point — preserving it would keep granting the waiver for the\n // rest of the staleness window after entitlement ended.\n return null;\n }\n if (!fallback) {\n // Nothing else can answer, so this rejection is the whole result.\n // Returning `null` here would be stored as a successful \"no\n // subscription\" snapshot and silently erase a valid cached one; let\n // it reach the refresh handler, which keeps the previous snapshot.\n throw error;\n }\n // A registered handler that rejects still falls back to the injected\n // source rather than erasing the cached snapshot.\n }\n }\n\n if (!fallback) {\n // No handler answered and no injected source: genuinely nothing to\n // report, which is a real `null` rather than a swallowed failure.\n return null;\n }\n\n return await fallback.getPerpsBenefits();\n }\n\n /**\n * Register the current HyperLiquid trading address with the subscription\n * profile, so a later fill decoded off the HL fan-out can be attributed.\n *\n * ADR 0064 calls for this at preview time and again whenever the selected\n * account changes. Registration is idempotent backend-side and deliberately\n * never throws: it is observability plumbing, and a failure here must not\n * block a fee preview.\n *\n * New clients may route this through the structural\n * `SubscriptionController:registerAddress` action. Older clients continue to\n * use the injected `subscription` hook as a compatibility fallback.\n *\n * @param address - The EVM trading address to register.\n * @param options - Registration options.\n * @param options.isTestnet - Whether the caller is trading against the\n * HyperLiquid testnet. Selects which HyperLiquid chain the CAIP-10 names;\n * defaults to mainnet.\n * @returns A promise that resolves once the attempt settles.\n */\n async registerTradingAddress(\n address: string,\n options?: { isTestnet?: boolean },\n ): Promise<void> {\n try {\n // HyperLiquid's own chain, not the wallet's selected network. The address\n // is being announced so a fill decoded off the HyperLiquid fan-out can be\n // matched back to a profile, and that fill names `eip155:999` /\n // `eip155:998`. Registering under whatever network the wallet happened to\n // have selected produces an identifier no fill will ever match.\n const chainId = options?.isTestnet\n ? HYPERLIQUID_TESTNET_CHAIN_ID\n : HYPERLIQUID_MAINNET_CHAIN_ID;\n\n const caipAccountId = formatAccountToCaipAccountId(\n address,\n chainId,\n this.#deps.logger,\n );\n\n if (!caipAccountId) {\n return;\n }\n\n // Skip the round trip when this address was already registered for this\n // session: preview runs on every keystroke in the order form.\n if (this.#registeredTradingAddresses.has(caipAccountId)) {\n return;\n }\n\n // Capture the identity before invoking either integration. If profile\n // invalidation happens while the handler is pending, its completion must\n // not repopulate the registration cache for the old profile.\n const epoch = this.#benefitsEpoch;\n\n // New clients can route registration through SubscriptionController.\n // Older clients do not expose this action and continue using the\n // injected hook below.\n let registration: Promise<void> | undefined;\n try {\n const result = this.#messenger.call(\n 'SubscriptionController:registerAddress',\n caipAccountId,\n );\n if (result !== undefined) {\n registration = Promise.resolve(result);\n }\n } catch (error) {\n if (\n !isMissingActionHandlerError(\n error,\n 'SubscriptionController:registerAddress',\n ) &&\n !this.#deps.subscription\n ) {\n throw error;\n }\n }\n\n if (!registration) {\n const source = this.#deps.subscription;\n if (!source?.registerTradingAddress) {\n // Visible rather than silent: a client wired only to benefits has no\n // way to register, and a missing registration is otherwise\n // indetectable until fills arrive unattributed.\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: No trading-address registration hook wired; fills will be unattributed',\n { address },\n );\n return;\n }\n registration = source.registerTradingAddress(caipAccountId);\n }\n\n // Fence the completion against an invalidation that lands while this\n // registration is in flight. `invalidateSubscriptionBenefits` clears the\n // registered set because the profile behind it changed; writing back\n // afterwards would re-add an address registered for the *previous*\n // profile, and the new profile would then skip its own registration and\n // leave its fills unattributable.\n await registration;\n if (epoch !== this.#benefitsEpoch) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registration superseded',\n { caipAccountId },\n );\n return;\n }\n this.#registeredTradingAddresses.add(caipAccountId);\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registered',\n { caipAccountId },\n );\n } catch (error) {\n // An offline client or a backend refusal both land here. Neither is a\n // reason to fail a fee preview.\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registration skipped',\n {\n address,\n error: ensureError(\n error,\n 'RewardsIntegrationService.registerTradingAddress',\n ).message,\n },\n );\n }\n }\n\n /**\n * Forget which trading addresses were registered this session.\n *\n * Called when the selected account changes, so the next preview re-sends the\n * registration for the new address rather than assuming the previous one\n * still stands.\n */\n resetRegisteredTradingAddresses(): void {\n this.#registeredTradingAddresses.clear();\n }\n\n /**\n * Log that the rewards discount is unavailable because the host does not\n * provide a NetworkController action.\n *\n * @param error - The missing-handler error from the messenger.\n */\n #logNetworkControllerNotProvided(error: unknown): void {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount unavailable (NetworkController not provided)',\n { error: ensureError(error).message },\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. A host without\n // NetworkController (or one that does not delegate it) simply has no\n // rewards discount: the fee falls back to the next source. Each read\n // checks only its own action, so a handler failing on a missing\n // dependency of its own is still reported below.\n let selectedNetworkClientId: string;\n try {\n ({ selectedNetworkClientId } = this.#messenger.call(\n 'NetworkController:getState',\n ));\n } catch (error) {\n if (!isMissingActionHandlerError(error, 'NetworkController:getState')) {\n throw error;\n }\n this.#logNetworkControllerNotProvided(error);\n return undefined;\n }\n\n let chainId: string | undefined;\n let chainIdError: string | undefined;\n try {\n chainId = this.#messenger.call(\n 'NetworkController:getNetworkClientById',\n selectedNetworkClientId,\n ).configuration.chainId;\n } catch (error) {\n if (\n isMissingActionHandlerError(\n error,\n 'NetworkController:getNetworkClientById',\n )\n ) {\n this.#logNetworkControllerNotProvided(error);\n return undefined;\n }\n // Network client may not exist; reported below with the cause.\n chainIdError = ensureError(error).message;\n }\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 ...(chainIdError && { error: chainIdError }),\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 * Whether a benefits rejection definitively means \"this profile is not\n * entitled\", as opposed to \"the read failed\".\n *\n * `SubscriptionController.getBenefits` throws `UserNotSubscribed` when the\n * subscription is inactive or the response reports ineligibility, and clears\n * its own benefits state on that path. Treating it as a transport failure would\n * keep serving a cached waiver for the rest of the staleness window — up to ten\n * minutes of free trading after entitlement ended — so it is converted to a\n * real `null` answer instead.\n *\n * Matched on the message because the controller throws a plain `Error`; any\n * other failure stays a failure and preserves the cached snapshot.\n *\n * @param error - The rejection from the benefits read.\n * @returns True when the rejection means the profile is not entitled.\n */\nfunction isNotSubscribedRejection(error: unknown): boolean {\n return (\n error instanceof Error && error.message.includes('User is not subscribed')\n );\n}\n\n/**\n * Convert `SubscriptionController`'s benefits response into the shape the\n * waiver gate reads.\n *\n * The controller reports allowances in **micro-USD** (`remainingMicroUsd`) and\n * carries no `status`/`entitled` fields: eligibility is the response's own\n * `eligible` flag, and the perps product block holds the cap. Converting once,\n * here at the boundary, keeps the gate and the blended-rate formula working in\n * whole USD.\n *\n * A `remainingMicroUsd` of `null` means the backend reported no bound, which\n * stays an unbounded allowance rather than a spent one.\n *\n * @param response - The controller's benefits response.\n * @returns The internal benefits shape, or null when nothing is entitled.\n */\nfunction adaptSubscriptionBenefits(\n response: SubscriptionBenefitsResponse | null | undefined,\n): PerpsSubscriptionBenefits | null {\n if (!response) {\n return null;\n }\n\n const perps = response.products?.perps;\n // `products.perps` is always present on the response, so its existence proves\n // nothing. Entitlement is the response-level `eligible` flag plus positive\n // evidence from the perps block itself: a builder fee rate to apply, or a\n // reported allowance to spend. A block carrying neither describes a profile\n // with no perps benefit, whatever the other products say.\n const hasPerpsBenefit = Boolean(\n perps &&\n (perps.builderFeeBips !== null ||\n perps.remainingMicroUsd !== null ||\n perps.capMicroUsd !== undefined),\n );\n\n return {\n status: response.eligible ? 'active' : 'inactive',\n perpsFeeWaiver: {\n entitled: response.eligible && hasPerpsBenefit,\n usage: perps?.exhausted ? 'exhausted' : 'available',\n exhausted: perps?.exhausted,\n remainingNotionalUsd:\n perps?.remainingMicroUsd === null ||\n perps?.remainingMicroUsd === undefined\n ? undefined\n : perps.remainingMicroUsd / MICRO_USD_PER_USD,\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
+ {"version":3,"file":"RewardsIntegrationService.js","sourceRoot":"","sources":["../../src/services/RewardsIntegrationService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SubscriptionBenefitsResponse } from '@metamask/subscription-controller';\n\nimport {\n BASIS_POINTS_DIVISOR,\n BUILDER_FEE_CONFIG,\n HYPERLIQUID_MAINNET_CHAIN_ID,\n HYPERLIQUID_TESTNET_CHAIN_ID,\n} from '../constants/hyperLiquidConfig.js';\nimport {\n PERPS_CONSTANTS,\n SUBSCRIPTION_BENEFITS_CACHE,\n SUBSCRIPTION_FEE_WAIVER_FLAG,\n} from '../constants/perpsConfig.js';\nimport type {\n PerpsFeeResolution,\n PerpsFeeResolverScope,\n PerpsFeeSource,\n PerpsPlatformDependencies,\n PerpsSubscriptionBenefits,\n PerpsSubscriptionFeeWaiverStatus,\n PerpsTradingFeeGrant,\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';\nimport {\n quantizeBuilderFeeTenthsBps,\n resolveSubscriptionWaiverRate,\n} from '../utils/subscriptionFeeWaiver.js';\nimport { isMissingActionHandlerError } from './missingActionHandler.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/** Number of venue fee increments in one basis point. */\nconst BUILDER_FEE_TENTHS_PER_BIP =\n BUILDER_FEE_CONFIG.MaxFeeTenthsBps / DEFAULT_FEE_BIPS;\n\n/**\n * Floor an absolute fee to the venue's integer tenths without dropping an\n * exact boundary because of multiplication error.\n *\n * The two-ULP tolerance is far smaller than a venue increment, but covers\n * values such as `0.29999999999999993` produced by ordinary decimal\n * arithmetic when the intended value is 0.3.\n *\n * @param feeBips - Absolute builder fee in basis points.\n * @returns The chargeable fee in integer tenths of a basis point.\n */\nfunction quantizeAbsoluteFeeTenthsBps(feeBips: number): number {\n const scaledFee = feeBips * BUILDER_FEE_TENTHS_PER_BIP;\n const tolerance = Number.EPSILON * Math.max(1, Math.abs(scaledFee)) * 2;\n\n return Math.floor(scaledFee + tolerance);\n}\n\n/** `SubscriptionController` reports allowances in micro-USD. */\nconst MICRO_USD_PER_USD = 1_000_000;\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` — the account-scoped VIP and season discount returned by\n * `rewards.getPerpsDiscountForAccount`.\n * - `grant` — an independent, expiring, route-scoped fee returned by\n * `rewards.getPerpsTradingFeeGrant`.\n * - `subscription` — `0` bips, but only when the eligibility gate passes on a\n * cached read of the profile's benefits.\n *\n * Rewards wins ties with default. Grant and subscription must be strictly\n * cheaper after venue quantization to win.\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 * CAIP-10 addresses already registered with the subscription profile this\n * session, so preview does not re-send the same registration on every\n * keystroke. Cleared on account switch and on cache invalidation.\n */\n readonly #registeredTradingAddresses = new Set<string>();\n\n /**\n * Whether `SubscriptionController:getBenefits` has ever answered on this\n * messenger. Action registration can be delegated, in which case it does not\n * appear in `getRegisteredActionTypes`, so an answered call is the only\n * reliable proof that the messenger route is available.\n */\n #messengerBenefitsAnswered = false;\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 * Calculate user fee discount from the unified fee resolver.\n * Returns discount in basis points (e.g., 6500 = 65% discount)\n *\n * @param orderNotionalUsd - Order notional (USD), when the caller knows it.\n * @param scope - Provider route used to request route-scoped fee sources.\n * Omission intentionally excludes grants for compatibility callers.\n * @returns The fee discount in basis points, or undefined if no source resolved.\n */\n async calculateUserFeeDiscount(\n orderNotionalUsd?: number,\n scope?: PerpsFeeResolverScope,\n ): Promise<number | undefined> {\n const resolution = await this.resolveFee(orderNotionalUsd, scope);\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 * The subscription source contributes an effective rate rather than a flat\n * zero (ADR 0064): the allowance may cover only part of the order, and the\n * blend that results has to be able to lose to a deeper VIP or season\n * discount. Passing the order notional is what makes that blend possible.\n *\n * Without a notional the outcome depends on whether the backend bounded the\n * allowance. An unbounded allowance still resolves to the full waiver — there\n * is no cap to over-consume. A *bounded* one is withheld entirely rather than\n * quoted as a full waiver, because charging nothing on an order of unknown\n * size would silently spend the cap; such a caller gets the next-lowest\n * source instead.\n *\n * @param orderNotionalUsd - Order notional (USD), when the caller knows it.\n * @param scope - Provider route used to request route-scoped fee sources.\n * Omission intentionally excludes grants.\n * @returns The winning fee, its source, and the subscription gate outcome.\n */\n async resolveFee(\n orderNotionalUsd?: number,\n scope?: PerpsFeeResolverScope,\n ): Promise<PerpsFeeResolution> {\n const [rewardsDiscount, grantCandidate] = await Promise.all([\n this.#calculateRewardsDiscount(),\n scope\n ? this.#getPerpsTradingFeeGrant(scope)\n : Promise.resolve<PerpsTradingFeeGrant | null>(null),\n ]);\n // Expiry is deliberately checked only after every concurrent candidate has\n // settled. A grant that expires while the account-scoped rewards request is\n // in flight must not be selected.\n const grant =\n scope && isValidTradingFeeGrant(grantCandidate, scope, Date.now())\n ? grantCandidate\n : undefined;\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 const toTenthsBps = (bips: number): number =>\n quantizeBuilderFeeTenthsBps(\n Math.round((1 - bips / DEFAULT_FEE_BIPS) * BASIS_POINTS_DIVISOR),\n );\n const fromTenthsBps = (tenthsBps: number): number =>\n tenthsBps / BUILDER_FEE_TENTHS_PER_BIP;\n\n if (rewardsDiscount !== undefined) {\n const rewardsFeeBips =\n DEFAULT_FEE_BIPS * (1 - rewardsDiscount / 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 // A grant only wins when it lowers the fee the venue will actually charge.\n // Rewards therefore keeps a quantized tie, and default keeps a grant that\n // does not reduce the on-wire rate.\n // Grants are absolute fees and may carry more precision than the legacy\n // integer discount. Quantize that absolute candidate first: converting a\n // 0.0999-bip grant to a discount first would round it to 9900 bips and\n // submit 0.1 bip instead of the venue-floored zero.\n const grantFeeTenthsBps = grant\n ? quantizeAbsoluteFeeTenthsBps(grant.feeBips)\n : undefined;\n if (\n grantFeeTenthsBps !== undefined &&\n grantFeeTenthsBps < toTenthsBps(feeBips)\n ) {\n feeBips = fromTenthsBps(grantFeeTenthsBps);\n source = 'grant';\n }\n\n const waiver = resolveSubscriptionWaiverRate({\n status: subscription,\n maxFeeBips: DEFAULT_FEE_BIPS,\n orderNotionalUsd,\n });\n\n let subscriptionWaiverKind: PerpsFeeResolution['subscriptionWaiverKind'];\n let subscriptionCoveredNotionalUsd: number | undefined;\n\n // Full and partial waivers compete with the rewards fee. Subscription\n // only wins when it produces a genuinely cheaper fee on the wire.\n // Compared *after* venue quantization, because that is the fee the order\n // pays. The builder fee is submitted in integer tenths of a basis point, so\n // a blend like 9.9999 bips is cheaper than the 10-bip default in arithmetic\n // and identical to it on the wire. Selecting subscription on the raw number\n // would label such an order `source: 'subscription'` with a 0-bip discount\n // while it pays full price, and would disagree with the cloid marker, which\n // already gates on the quantized fee.\n // Subscription must be strictly cheaper on the wire to claim the order. A\n // tie buys the user nothing — the same rate is already available from the\n // source that won — while claiming it marks the cloid and spends the\n // remaining allowance, so a tie is left to the other source. That holds at\n // any rate, including a rewards discount that already reaches 0 bips.\n const waiverSurvivesQuantization =\n toTenthsBps(waiver.feeBips) < toTenthsBps(feeBips);\n\n if (\n waiver.applies &&\n waiver.feeBips <= feeBips &&\n waiverSurvivesQuantization\n ) {\n feeBips = waiver.feeBips;\n source = 'subscription';\n subscriptionWaiverKind = waiver.kind === 'partial' ? 'partial' : 'full';\n subscriptionCoveredNotionalUsd = waiver.coveredNotionalUsd;\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: rewardsDiscount,\n grantFeeBips: grant?.feeBips,\n grantExpiresAt: grant?.expiresAt,\n orderNotionalUsd,\n subscriptionEligible: subscription.eligible,\n subscriptionReason: subscription.reason,\n subscriptionWaiverKind,\n subscriptionCoveredNotionalUsd,\n });\n\n return {\n feeBips,\n discountBips,\n source,\n subscription,\n subscriptionWaiverKind,\n subscriptionCoveredNotionalUsd,\n };\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.#hasSubscriptionSource()) {\n return { eligible: false, reason: 'no-source' };\n }\n\n // ADR 0064 Milestone 8: the subscription source has to be killable on its\n // own. Reported as `no-source` so a disabled flag is indistinguishable from\n // an unwired client downstream — both mean \"subscription contributes\n // nothing\", and neither touches rewards or the default fee.\n if (!this.#isSubscriptionFeeWaiverEnabled()) {\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 * Whether this client has any way to read subscription benefits.\n *\n * Either wiring counts: a registered `SubscriptionController:getBenefits`\n * action (the ADR 0064 target) or the legacy injected `subscription` callback.\n * Requiring the injected one would make the messenger path unreachable on\n * exactly the configuration it was added for, so the messenger is probed by\n * asking whether an action handler exists rather than by calling it — this\n * runs on the synchronous status read and must not start work.\n *\n * @returns True when benefits can be read by some route.\n */\n #hasSubscriptionSource(): boolean {\n if (this.#deps.subscription || this.#messengerBenefitsAnswered) {\n return true;\n }\n\n try {\n // `getRegisteredActionTypes` reports this messenger's own registrations.\n // A delegated action does not appear there, which is why it is only a\n // positive signal — `refreshSubscriptionBenefits` still attempts the call\n // regardless, and an answer sets `#messengerBenefitsAnswered` above.\n return this.#messenger\n .getRegisteredActionTypes()\n .includes('SubscriptionController:getBenefits');\n } catch {\n return false;\n }\n }\n\n /**\n * Whether the subscription fee-waiver source is enabled remotely.\n *\n * Fails open: an absent flag, a malformed value, or an unreachable flag\n * controller all read as enabled, because silently dropping a benefit the\n * user pays for is worse than serving it one release too long. The kill\n * switch is an explicit `false`.\n *\n * @returns True unless the remote flag explicitly disables the source.\n */\n #isSubscriptionFeeWaiverEnabled(): boolean {\n try {\n const { remoteFeatureFlags } = this.#messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n const flag = remoteFeatureFlags?.[SUBSCRIPTION_FEE_WAIVER_FLAG];\n return flag !== false;\n } catch {\n // No flag controller registered, or the read threw: keep the source.\n return true;\n }\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 // Deliberately not gated on `#hasSubscriptionSource`: a delegated action is\n // invisible to `getRegisteredActionTypes`, so the first call is what proves\n // it is reachable. A client with no source at all reads `null` here, which\n // costs one no-op call per freshness window and leaves the gate closed.\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();\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 // The identity behind the registration changed too, so the new one has to\n // announce itself rather than inherit the previous profile's registration.\n this.#registeredTradingAddresses.clear();\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 async #readSubscriptionBenefits(): Promise<void> {\n const epoch = this.#benefitsEpoch;\n\n try {\n const benefits = await this.#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 * Read subscription benefits, preferring the messenger over the DI callback.\n *\n * ADR 0064 moves hydration onto `SubscriptionController`. The messenger is\n * tried first and the injected `subscription` dependency is only a fallback,\n * so a client that ships `SubscriptionController` without the legacy callback\n * hydrates normally — that configuration is the ADR's target, not an edge\n * case. A client with neither gets `null`, which reads as \"no subscription\".\n *\n * @returns The benefits payload, or null when there is none to report.\n */\n async #getPerpsBenefits(): Promise<PerpsSubscriptionBenefits | null> {\n let pending: Promise<PerpsSubscriptionBenefits | null> | undefined;\n try {\n // Called without awaiting so the fallback stays synchronous when no\n // handler is registered: the DI read must start in the same tick, or a\n // caller that inspects the in-flight state sees an idle service.\n const result = this.#messenger.call('SubscriptionController:getBenefits');\n // `undefined` means nothing handled the action, which is the fallback\n // case rather than an answer.\n if (result !== undefined) {\n this.#messengerBenefitsAnswered = true;\n pending = Promise.resolve(result).then(adaptSubscriptionBenefits);\n }\n } catch (error) {\n // A handler that has answered before exists, so a synchronous throw is a\n // real failure rather than an unregistered action. Treating it as the\n // latter would fall through to `null` and erase a valid cached snapshot,\n // exactly as an asynchronous rejection would.\n if (isNotSubscribedRejection(error)) {\n // Definitive \"not entitled\", even thrown synchronously.\n return null;\n }\n // An unregistered or undelegated action throws a recognisable \"no\n // handler\" error; any other synchronous throw came from a handler that\n // exists and failed, so it must not be mistaken for an absent action and\n // cached as `null` — including on the very first call, before one has\n // ever answered.\n if (\n !isMissingActionHandlerError(\n error,\n 'SubscriptionController:getBenefits',\n ) &&\n !this.#deps.subscription\n ) {\n throw error;\n }\n // Otherwise: missing handler, or a throw with a DI source to fall\n // back to.\n }\n\n const fallback = this.#deps.subscription;\n\n if (pending) {\n try {\n return await pending;\n } catch (error) {\n if (isNotSubscribedRejection(error)) {\n // A definitive answer, not a failed read: the profile is not\n // entitled. Returning `null` replaces the cached snapshot, which is\n // the point — preserving it would keep granting the waiver for the\n // rest of the staleness window after entitlement ended.\n return null;\n }\n if (!fallback) {\n // Nothing else can answer, so this rejection is the whole result.\n // Returning `null` here would be stored as a successful \"no\n // subscription\" snapshot and silently erase a valid cached one; let\n // it reach the refresh handler, which keeps the previous snapshot.\n throw error;\n }\n // A registered handler that rejects still falls back to the injected\n // source rather than erasing the cached snapshot.\n }\n }\n\n if (!fallback) {\n // No handler answered and no injected source: genuinely nothing to\n // report, which is a real `null` rather than a swallowed failure.\n return null;\n }\n\n return await fallback.getPerpsBenefits();\n }\n\n /**\n * Register the current HyperLiquid trading address with the subscription\n * profile, so a later fill decoded off the HL fan-out can be attributed.\n *\n * ADR 0064 calls for this at preview time and again whenever the selected\n * account changes. Registration is idempotent backend-side and deliberately\n * never throws: it is observability plumbing, and a failure here must not\n * block a fee preview.\n *\n * New clients may route this through the structural\n * `SubscriptionController:registerAddress` action. Older clients continue to\n * use the injected `subscription` hook as a compatibility fallback.\n *\n * @param address - The EVM trading address to register.\n * @param options - Registration options.\n * @param options.isTestnet - Whether the caller is trading against the\n * HyperLiquid testnet. Selects which HyperLiquid chain the CAIP-10 names;\n * defaults to mainnet.\n * @returns A promise that resolves once the attempt settles.\n */\n async registerTradingAddress(\n address: string,\n options?: { isTestnet?: boolean },\n ): Promise<void> {\n try {\n // HyperLiquid's own chain, not the wallet's selected network. The address\n // is being announced so a fill decoded off the HyperLiquid fan-out can be\n // matched back to a profile, and that fill names `eip155:999` /\n // `eip155:998`. Registering under whatever network the wallet happened to\n // have selected produces an identifier no fill will ever match.\n const chainId = options?.isTestnet\n ? HYPERLIQUID_TESTNET_CHAIN_ID\n : HYPERLIQUID_MAINNET_CHAIN_ID;\n\n const caipAccountId = formatAccountToCaipAccountId(\n address,\n chainId,\n this.#deps.logger,\n );\n\n if (!caipAccountId) {\n return;\n }\n\n // Skip the round trip when this address was already registered for this\n // session: preview runs on every keystroke in the order form.\n if (this.#registeredTradingAddresses.has(caipAccountId)) {\n return;\n }\n\n // Capture the identity before invoking either integration. If profile\n // invalidation happens while the handler is pending, its completion must\n // not repopulate the registration cache for the old profile.\n const epoch = this.#benefitsEpoch;\n\n // New clients can route registration through SubscriptionController.\n // Older clients do not expose this action and continue using the\n // injected hook below.\n let registration: Promise<void> | undefined;\n try {\n const result = this.#messenger.call(\n 'SubscriptionController:registerAddress',\n caipAccountId,\n );\n if (result !== undefined) {\n registration = Promise.resolve(result);\n }\n } catch (error) {\n if (\n !isMissingActionHandlerError(\n error,\n 'SubscriptionController:registerAddress',\n ) &&\n !this.#deps.subscription\n ) {\n throw error;\n }\n }\n\n if (!registration) {\n const source = this.#deps.subscription;\n if (!source?.registerTradingAddress) {\n // Visible rather than silent: a client wired only to benefits has no\n // way to register, and a missing registration is otherwise\n // indetectable until fills arrive unattributed.\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: No trading-address registration hook wired; fills will be unattributed',\n { address },\n );\n return;\n }\n registration = source.registerTradingAddress(caipAccountId);\n }\n\n // Fence the completion against an invalidation that lands while this\n // registration is in flight. `invalidateSubscriptionBenefits` clears the\n // registered set because the profile behind it changed; writing back\n // afterwards would re-add an address registered for the *previous*\n // profile, and the new profile would then skip its own registration and\n // leave its fills unattributable.\n await registration;\n if (epoch !== this.#benefitsEpoch) {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registration superseded',\n { caipAccountId },\n );\n return;\n }\n this.#registeredTradingAddresses.add(caipAccountId);\n\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registered',\n { caipAccountId },\n );\n } catch (error) {\n // An offline client or a backend refusal both land here. Neither is a\n // reason to fail a fee preview.\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Trading address registration skipped',\n {\n address,\n error: ensureError(\n error,\n 'RewardsIntegrationService.registerTradingAddress',\n ).message,\n },\n );\n }\n }\n\n /**\n * Forget which trading addresses were registered this session.\n *\n * Called when the selected account changes, so the next preview re-sends the\n * registration for the new address rather than assuming the previous one\n * still stands.\n */\n resetRegisteredTradingAddresses(): void {\n this.#registeredTradingAddresses.clear();\n }\n\n /**\n * Log that the rewards discount is unavailable because the host does not\n * provide a NetworkController action.\n *\n * @param error - The missing-handler error from the messenger.\n */\n #logNetworkControllerNotProvided(error: unknown): void {\n this.#deps.debugLogger.log(\n 'RewardsIntegrationService: Fee discount unavailable (NetworkController not provided)',\n { error: ensureError(error).message },\n );\n }\n\n /**\n * Resolve the VIP and season discount for the selected account.\n *\n * @returns The numeric discount, 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. A host without\n // NetworkController (or one that does not delegate it) simply has no\n // rewards discount: the fee falls back to the next source. Each read\n // checks only its own action, so a handler failing on a missing\n // dependency of its own is still reported below.\n let selectedNetworkClientId: string;\n try {\n ({ selectedNetworkClientId } = this.#messenger.call(\n 'NetworkController:getState',\n ));\n } catch (error) {\n if (!isMissingActionHandlerError(error, 'NetworkController:getState')) {\n throw error;\n }\n this.#logNetworkControllerNotProvided(error);\n return undefined;\n }\n\n let chainId: string | undefined;\n let chainIdError: string | undefined;\n try {\n chainId = this.#messenger.call(\n 'NetworkController:getNetworkClientById',\n selectedNetworkClientId,\n ).configuration.chainId;\n } catch (error) {\n if (\n isMissingActionHandlerError(\n error,\n 'NetworkController:getNetworkClientById',\n )\n ) {\n this.#logNetworkControllerNotProvided(error);\n return undefined;\n }\n // Network client may not exist; reported below with the cause.\n chainIdError = ensureError(error).message;\n }\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 ...(chainIdError && { error: chainIdError }),\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 * Read the independent trading-fee grant for a provider route.\n *\n * @param scope - Exact route requested by the resolver.\n * @returns The candidate, or null when unsupported or unavailable.\n */\n async #getPerpsTradingFeeGrant(\n scope: PerpsFeeResolverScope,\n ): Promise<PerpsTradingFeeGrant | null> {\n if (!this.#deps.rewards.getPerpsTradingFeeGrant) {\n return null;\n }\n\n try {\n return await this.#deps.rewards.getPerpsTradingFeeGrant(scope);\n } catch (error) {\n this.#deps.logger.error(\n ensureError(error, 'RewardsIntegrationService.getPerpsTradingFeeGrant'),\n {\n tags: { feature: PERPS_CONSTANTS.FeatureName },\n context: {\n name: 'RewardsIntegrationService.getPerpsTradingFeeGrant',\n data: {},\n },\n },\n );\n return null;\n }\n }\n}\n\n/**\n * Validate a trading-fee grant at the time fee selection occurs.\n *\n * @param grant - Candidate returned by the client-owned RewardsController.\n * @param scope - Exact provider route requested by the resolver.\n * @param now - Selection time as Unix milliseconds.\n * @returns Whether the candidate is valid, unexpired, and matches the route.\n */\nfunction isValidTradingFeeGrant(\n grant: PerpsTradingFeeGrant | null,\n scope: PerpsFeeResolverScope,\n now: number,\n): grant is PerpsTradingFeeGrant {\n return Boolean(\n grant &&\n grant.providerId === scope.providerId &&\n grant.isTestnet === scope.isTestnet &&\n Number.isFinite(grant.feeBips) &&\n grant.feeBips >= 0 &&\n Number.isFinite(grant.expiresAt) &&\n now < grant.expiresAt,\n );\n}\n\n/**\n * Whether a benefits rejection definitively means \"this profile is not\n * entitled\", as opposed to \"the read failed\".\n *\n * `SubscriptionController.getBenefits` throws `UserNotSubscribed` when the\n * subscription is inactive or the response reports ineligibility, and clears\n * its own benefits state on that path. Treating it as a transport failure would\n * keep serving a cached waiver for the rest of the staleness window — up to ten\n * minutes of free trading after entitlement ended — so it is converted to a\n * real `null` answer instead.\n *\n * Matched on the message because the controller throws a plain `Error`; any\n * other failure stays a failure and preserves the cached snapshot.\n *\n * @param error - The rejection from the benefits read.\n * @returns True when the rejection means the profile is not entitled.\n */\nfunction isNotSubscribedRejection(error: unknown): boolean {\n return (\n error instanceof Error && error.message.includes('User is not subscribed')\n );\n}\n\n/**\n * Convert `SubscriptionController`'s benefits response into the shape the\n * waiver gate reads.\n *\n * The controller reports allowances in **micro-USD** (`remainingMicroUsd`) and\n * carries no `status`/`entitled` fields: eligibility is the response's own\n * `eligible` flag, and the perps product block holds the cap. Converting once,\n * here at the boundary, keeps the gate and the blended-rate formula working in\n * whole USD.\n *\n * A `remainingMicroUsd` of `null` means the backend reported no bound, which\n * stays an unbounded allowance rather than a spent one.\n *\n * @param response - The controller's benefits response.\n * @returns The internal benefits shape, or null when nothing is entitled.\n */\nfunction adaptSubscriptionBenefits(\n response: SubscriptionBenefitsResponse | null | undefined,\n): PerpsSubscriptionBenefits | null {\n if (!response) {\n return null;\n }\n\n const perps = response.products?.perps;\n // `products.perps` is always present on the response, so its existence proves\n // nothing. Entitlement is the response-level `eligible` flag plus positive\n // evidence from the perps block itself: a builder fee rate to apply, or a\n // reported allowance to spend. A block carrying neither describes a profile\n // with no perps benefit, whatever the other products say.\n const hasPerpsBenefit = Boolean(\n perps &&\n (perps.builderFeeBips !== null ||\n perps.remainingMicroUsd !== null ||\n perps.capMicroUsd !== undefined),\n );\n\n return {\n status: response.eligible ? 'active' : 'inactive',\n perpsFeeWaiver: {\n entitled: response.eligible && hasPerpsBenefit,\n usage: perps?.exhausted ? 'exhausted' : 'available',\n exhausted: perps?.exhausted,\n remainingNotionalUsd:\n perps?.remainingMicroUsd === null ||\n perps?.remainingMicroUsd === undefined\n ? undefined\n : perps.remainingMicroUsd / MICRO_USD_PER_USD,\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":"TradingService.d.ts","sourceRoot":"","sources":["../../src/services/TradingService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TradingService.d.ts","sourceRoot":"","sources":["../../src/services/TradingService.ts"],"names":[],"mappings":"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"}