@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.
- package/CHANGELOG.md +30 -1
- package/dist/PerpsController-method-action-types.cjs.map +1 -1
- package/dist/PerpsController-method-action-types.d.cts +36 -1
- package/dist/PerpsController-method-action-types.d.cts.map +1 -1
- package/dist/PerpsController-method-action-types.d.mts +36 -1
- package/dist/PerpsController-method-action-types.d.mts.map +1 -1
- package/dist/PerpsController-method-action-types.mjs.map +1 -1
- package/dist/PerpsController.cjs +600 -119
- package/dist/PerpsController.cjs.map +1 -1
- package/dist/PerpsController.d.cts +35 -4
- package/dist/PerpsController.d.cts.map +1 -1
- package/dist/PerpsController.d.mts +35 -4
- package/dist/PerpsController.d.mts.map +1 -1
- package/dist/PerpsController.mjs +603 -122
- package/dist/PerpsController.mjs.map +1 -1
- package/dist/constants/eventNames.cjs +3 -0
- package/dist/constants/eventNames.cjs.map +1 -1
- package/dist/constants/eventNames.d.cts +1 -0
- package/dist/constants/eventNames.d.cts.map +1 -1
- package/dist/constants/eventNames.d.mts +1 -0
- package/dist/constants/eventNames.d.mts.map +1 -1
- package/dist/constants/eventNames.mjs +3 -0
- package/dist/constants/eventNames.mjs.map +1 -1
- package/dist/constants/hyperLiquidConfig.cjs +14 -2
- package/dist/constants/hyperLiquidConfig.cjs.map +1 -1
- package/dist/constants/hyperLiquidConfig.d.cts +7 -0
- package/dist/constants/hyperLiquidConfig.d.cts.map +1 -1
- package/dist/constants/hyperLiquidConfig.d.mts +7 -0
- package/dist/constants/hyperLiquidConfig.d.mts.map +1 -1
- package/dist/constants/hyperLiquidConfig.mjs +11 -0
- package/dist/constants/hyperLiquidConfig.mjs.map +1 -1
- package/dist/constants/perpsConfig.cjs +19 -2
- package/dist/constants/perpsConfig.cjs.map +1 -1
- package/dist/constants/perpsConfig.d.cts +39 -7
- package/dist/constants/perpsConfig.d.cts.map +1 -1
- package/dist/constants/perpsConfig.d.mts +39 -7
- package/dist/constants/perpsConfig.d.mts.map +1 -1
- package/dist/constants/perpsConfig.mjs +18 -1
- package/dist/constants/perpsConfig.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.cjs +16 -0
- package/dist/providers/AggregatedPerpsProvider.cjs.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.d.cts +3 -1
- package/dist/providers/AggregatedPerpsProvider.d.cts.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.d.mts +3 -1
- package/dist/providers/AggregatedPerpsProvider.d.mts.map +1 -1
- package/dist/providers/AggregatedPerpsProvider.mjs +16 -0
- package/dist/providers/AggregatedPerpsProvider.mjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.cjs +325 -59
- package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.cts +27 -1
- package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.mts +27 -1
- package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
- package/dist/providers/HyperLiquidProvider.mjs +326 -60
- package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
- package/dist/services/HyperLiquidClientService.cjs +74 -25
- package/dist/services/HyperLiquidClientService.cjs.map +1 -1
- package/dist/services/HyperLiquidClientService.d.cts.map +1 -1
- package/dist/services/HyperLiquidClientService.d.mts.map +1 -1
- package/dist/services/HyperLiquidClientService.mjs +74 -25
- package/dist/services/HyperLiquidClientService.mjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.cjs +16 -5
- package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.cts +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.mts +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.mjs +16 -5
- package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
- package/dist/services/MarketDataService.cjs +49 -5
- package/dist/services/MarketDataService.cjs.map +1 -1
- package/dist/services/MarketDataService.d.cts +2 -2
- package/dist/services/MarketDataService.d.cts.map +1 -1
- package/dist/services/MarketDataService.d.mts +2 -2
- package/dist/services/MarketDataService.d.mts.map +1 -1
- package/dist/services/MarketDataService.mjs +49 -5
- package/dist/services/MarketDataService.mjs.map +1 -1
- package/dist/services/RewardsIntegrationService.cjs +330 -68
- package/dist/services/RewardsIntegrationService.cjs.map +1 -1
- package/dist/services/RewardsIntegrationService.d.cts +61 -5
- package/dist/services/RewardsIntegrationService.d.cts.map +1 -1
- package/dist/services/RewardsIntegrationService.d.mts +61 -5
- package/dist/services/RewardsIntegrationService.d.mts.map +1 -1
- package/dist/services/RewardsIntegrationService.mjs +330 -68
- package/dist/services/RewardsIntegrationService.mjs.map +1 -1
- package/dist/services/ServiceContext.cjs.map +1 -1
- package/dist/services/ServiceContext.d.cts +17 -1
- package/dist/services/ServiceContext.d.cts.map +1 -1
- package/dist/services/ServiceContext.d.mts +17 -1
- package/dist/services/ServiceContext.d.mts.map +1 -1
- package/dist/services/ServiceContext.mjs.map +1 -1
- package/dist/services/TerminalMarketService.cjs +422 -7
- package/dist/services/TerminalMarketService.cjs.map +1 -1
- package/dist/services/TerminalMarketService.d.cts +10 -1
- package/dist/services/TerminalMarketService.d.cts.map +1 -1
- package/dist/services/TerminalMarketService.d.mts +10 -1
- package/dist/services/TerminalMarketService.d.mts.map +1 -1
- package/dist/services/TerminalMarketService.mjs +423 -8
- package/dist/services/TerminalMarketService.mjs.map +1 -1
- package/dist/services/TradingService.cjs +52 -28
- package/dist/services/TradingService.cjs.map +1 -1
- package/dist/services/TradingService.d.cts.map +1 -1
- package/dist/services/TradingService.d.mts.map +1 -1
- package/dist/services/TradingService.mjs +52 -28
- package/dist/services/TradingService.mjs.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +156 -7
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.mts +156 -7
- package/dist/types/index.d.mts.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/utils/marketUtils.cjs +16 -1
- package/dist/utils/marketUtils.cjs.map +1 -1
- package/dist/utils/marketUtils.d.cts +1 -0
- package/dist/utils/marketUtils.d.cts.map +1 -1
- package/dist/utils/marketUtils.d.mts +1 -0
- package/dist/utils/marketUtils.d.mts.map +1 -1
- package/dist/utils/marketUtils.mjs +14 -0
- package/dist/utils/marketUtils.mjs.map +1 -1
- package/dist/utils/perpsDiskPersistence.cjs +19 -12
- package/dist/utils/perpsDiskPersistence.cjs.map +1 -1
- package/dist/utils/perpsDiskPersistence.d.cts +5 -1
- package/dist/utils/perpsDiskPersistence.d.cts.map +1 -1
- package/dist/utils/perpsDiskPersistence.d.mts +5 -1
- package/dist/utils/perpsDiskPersistence.d.mts.map +1 -1
- package/dist/utils/perpsDiskPersistence.mjs +19 -12
- package/dist/utils/perpsDiskPersistence.mjs.map +1 -1
- 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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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":"
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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
|
|
87
|
+
* @returns The fee discount in basis points, or undefined if no source resolved.
|
|
45
88
|
*/
|
|
46
89
|
async calculateUserFeeDiscount() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
305
|
+
return undefined;
|
|
102
306
|
}
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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":";;;;;;;;;;;;AAAA,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EACnB,2CAA0C;AAC3C,OAAO,EACL,eAAe,EACf,2BAA2B,EAC5B,qCAAoC;AASrC,OAAO,EAAE,kCAAkC,EAAE,kCAAiC;AAC9E,OAAO,EAAE,WAAW,EAAE,gCAA+B;AACrD,OAAO,EAAE,4BAA4B,EAAE,kCAAiC;AAExE;;;GAGG;AACH,MAAM,gBAAgB,GACpB,kBAAkB,CAAC,aAAa,GAAG,oBAAoB,CAAC;AAU1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAO,yBAAyB;IA8BpC;;;;;OAKG;IACH,YACE,IAA+B,EAC/B,SAAuC;;QArChC,kDAAiC;QAEjC,uDAAyC;QAElD,wEAAwE;QACxE,8DAAgD;QAEhD;;;;;;WAMG;QACH,2DAAmC;QAEnC,6DAA6D;QAC7D,6DAA4C;QAE5C;;;;;;;WAOG;QACH,mDAAiB,CAAC,EAAC;QAYjB,uBAAA,IAAI,mCAAS,IAAI,MAAA,CAAC;QAClB,uBAAA,IAAI,wCAAc,SAAS,MAAA,CAAC;IAC9B,CAAC;IAqBD;;;;;OAKG;IACH,KAAK,CAAC,wBAAwB;QAC5B,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAC3C,OAAO,UAAU,CAAC,YAAY,CAAC;IACjC,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU;QACd,MAAM,mBAAmB,GAAG,MAAM,uBAAA,IAAI,iGAA0B,MAA9B,IAAI,CAA4B,CAAC;QACnE,4EAA4E;QAC5E,gDAAgD;QAChD,MAAM,YAAY,GAAG,IAAI,CAAC,8BAA8B,EAAE,CAAC;QAE3D,IAAI,OAAO,GAAG,gBAAgB,CAAC;QAC/B,IAAI,MAAM,GAAmB,SAAS,CAAC;QAEvC,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;YACtC,MAAM,cAAc,GAClB,gBAAgB,GAAG,CAAC,CAAC,GAAG,mBAAmB,GAAG,oBAAoB,CAAC,CAAC;YACtE,yEAAyE;YACzE,iEAAiE;YACjE,IAAI,cAAc,IAAI,OAAO,EAAE,CAAC;gBAC9B,OAAO,GAAG,cAAc,CAAC;gBACzB,MAAM,GAAG,SAAS,CAAC;YACrB,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,IAAI,YAAY,CAAC,QAAQ,EAAE,CAAC;YAC1B,OAAO,GAAG,CAAC,CAAC;YACZ,MAAM,GAAG,cAAc,CAAC;QAC1B,CAAC;QAED,MAAM,YAAY,GAChB,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,gBAAgB,CAAC,GAAG,oBAAoB,CAAC,CAAC;QAE1E,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CAAC,yCAAyC,EAAE;YACpE,MAAM;YACN,OAAO;YACP,YAAY;YACZ,cAAc,EAAE,gBAAgB;YAChC,mBAAmB;YACnB,oBAAoB,EAAE,YAAY,CAAC,QAAQ;YAC3C,kBAAkB,EAAE,YAAY,CAAC,MAAM;SACxC,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACzD,CAAC;IAED;;;;;;;OAOG;IACH,8BAA8B;QAC5B,IAAI,CAAC,uBAAA,IAAI,uCAAM,CAAC,YAAY,EAAE,CAAC;YAC7B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAClD,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,uBAAA,IAAI,mDAAkB,CAAC;QACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC3D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACrD,CAAC;QAED,IAAI,GAAG,GAAG,2BAA2B,CAAC,UAAU,EAAE,CAAC;YACjD,yEAAyE;YACzE,iEAAiE;YACjE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAC9C,CAAC;QAED,OAAO,qBAAqB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,2BAA2B;QAC/B,MAAM,MAAM,GAAG,uBAAA,IAAI,uCAAM,CAAC,YAAY,CAAC;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QAED,IAAI,uBAAA,IAAI,kDAAiB,EAAE,CAAC;YAC1B,MAAM,uBAAA,IAAI,kDAAiB,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,uBAAA,IAAI,mDAAkB;YACxC,CAAC,CAAC,GAAG,GAAG,uBAAA,IAAI,mDAAkB,CAAC,SAAS;YACxC,CAAC,CAAC,QAAQ,CAAC;QACb,MAAM,YAAY,GAChB,uBAAA,IAAI,gDAAe,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,uBAAA,IAAI,gDAAe,CAAC;QAC3E,IACE,WAAW,GAAG,2BAA2B,CAAC,OAAO;YACjD,YAAY,GAAG,2BAA2B,CAAC,OAAO,EAClD,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,uBAAA,IAAI,iGAA0B,MAA9B,IAAI,EAA2B,MAAM,CAAC,CAAC;QACvD,uBAAA,IAAI,8CAAoB,OAAO,MAAA,CAAC;QAChC,wEAAwE;QACxE,OAAO;aACJ,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,uBAAA,IAAI,kDAAiB,KAAK,OAAO,EAAE,CAAC;gBACtC,uBAAA,IAAI,8CAAoB,SAAS,MAAA,CAAC;YACpC,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAE1B,MAAM,OAAO,CAAC;IAChB,CAAC;IAED;;;;;;;;OAQG;IACH,8BAA8B;QAC5B,uBAAA,IAAI,+CAAqB,SAAS,MAAA,CAAC;QACnC,uBAAA,IAAI,4CAAkB,SAAS,MAAA,CAAC;QAChC,4EAA4E;QAC5E,yDAAyD;QACzD,qJAAuB,CAAC,MAAA,CAAC;QACzB,wEAAwE;QACxE,sEAAsE;QACtE,0EAA0E;QAC1E,kEAAkE;QAClE,uBAAA,IAAI,8CAAoB,SAAS,MAAA,CAAC;QAElC,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,oEAAoE,CACrE,CAAC;IACJ,CAAC;CA2KF;qfA5VuB,eAAuB;IAC3C,IAAI,CAAC;QACH,MAAM,aAAa,GAAG,uBAAA,IAAI,4CAAW,CAAC,IAAI,CACxC,wCAAwC,EACxC,eAAe,CAChB,CAAC;QACF,OAAO,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,+BAA+B;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAwKD;;;;;GAKG;AACH,KAAK,8DACH,MAA8D;IAE9D,MAAM,KAAK,GAAG,uBAAA,IAAI,gDAAe,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAEjD,IAAI,KAAK,KAAK,uBAAA,IAAI,gDAAe,EAAE,CAAC;YAClC,sEAAsE;YACtE,uDAAuD;YACvD,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,8EAA8E,CAC/E,CAAC;YACF,OAAO;QACT,CAAC;QAED,uBAAA,IAAI,+CAAqB,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,MAAA,CAAC;QAE7D,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,4DAA4D,EAC5D;YACE,MAAM,EAAE,QAAQ,EAAE,MAAM;YACxB,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ;YAC5C,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK;YACtC,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS;SAC/C,CACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,wEAAwE;QACxE,kEAAkE;QAClE,uBAAA,IAAI,uCAAM,CAAC,MAAM,CAAC,KAAK,CACrB,WAAW,CACT,KAAK,EACL,uDAAuD,CACxD,EACD;YACE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE;YAC9C,OAAO,EAAE;gBACP,IAAI,EAAE,uDAAuD;gBAC7D,IAAI,EAAE,EAAE;aACT;SACF,CACF,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,uEAAuE;QACvE,iEAAiE;QACjE,yEAAyE;QACzE,qCAAqC;QACrC,IAAI,KAAK,KAAK,uBAAA,IAAI,gDAAe,EAAE,CAAC;YAClC,uBAAA,IAAI,4CAAkB,IAAI,CAAC,GAAG,EAAE,MAAA,CAAC;QACnC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,KAAK;IACH,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,kCAAkC,CAAC,uBAAA,IAAI,4CAAW,CAAC,CAAC;QAEvE,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,kEAAkE,CACnE,CAAC;YACF,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,6CAA6C;QAC7C,MAAM,YAAY,GAAG,uBAAA,IAAI,4CAAW,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACxE,MAAM,EAAE,uBAAuB,EAAE,GAAG,YAAY,CAAC;QACjD,MAAM,OAAO,GAAG,uBAAA,IAAI,6FAAsB,MAA1B,IAAI,EAAuB,uBAAuB,CAAC,CAAC;QAEpE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,uBAAA,IAAI,uCAAM,CAAC,MAAM,CAAC,KAAK,CACrB,IAAI,KAAK,CAAC,iDAAiD,CAAC,EAC5D;gBACE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE;gBAC9C,OAAO,EAAE;oBACP,IAAI,EAAE,oDAAoD;oBAC1D,IAAI,EAAE;wBACJ,uBAAuB;qBACxB;iBACF;aACF,CACF,CAAC;YACF,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,kFAAkF;QAClF,MAAM,aAAa,GAAG,4BAA4B,CAChD,UAAU,CAAC,OAAO,EAClB,OAAO,EACP,uBAAA,IAAI,uCAAM,CAAC,MAAM,CAClB,CAAC;QAEF,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,uBAAA,IAAI,uCAAM,CAAC,MAAM,CAAC,KAAK,CACrB,IAAI,KAAK,CAAC,mDAAmD,CAAC,EAC9D;gBACE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE;gBAC9C,OAAO,EAAE;oBACP,IAAI,EAAE,oDAAoD;oBAC1D,IAAI,EAAE;wBACJ,OAAO,EAAE,UAAU,CAAC,OAAO;wBAC3B,OAAO;wBACP,uBAAuB;qBACxB;iBACF;aACF,CACF,CAAC;YACF,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,yDAAyD;QACzD,sEAAsE;QACtE,gEAAgE;QAChE,MAAM,YAAY,GAAG,MAAM,uBAAA,IAAI,uCAAM,CAAC,OAAO,CAAC,0BAA0B,CACtE,aAAa,EACb,gBAAgB,CACjB,CAAC;QAEF,sEAAsE;QACtE,+DAA+D;QAC/D,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAC1B,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,uFAAuF,EACvF,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,CAC/C,CAAC;YACF,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,uBAAA,IAAI,uCAAM,CAAC,WAAW,CAAC,GAAG,CACxB,oDAAoD,EACpD;YACE,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,aAAa;YACb,YAAY;YACZ,kBAAkB,EAAE,YAAY,GAAG,GAAG;SACvC,CACF,CAAC;QAEF,OAAO,YAAY,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,uBAAA,IAAI,uCAAM,CAAC,MAAM,CAAC,KAAK,CACrB,WAAW,CACT,KAAK,EACL,oDAAoD,CACrD,EACD;YACE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE;YAC9C,OAAO,EAAE;gBACP,IAAI,EAAE,oDAAoD;gBAC1D,IAAI,EAAE,EAAE;aACT;SACF,CACF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAGH;;;;;;;;;;;;GAYG;AACH,SAAS,qBAAqB,CAC5B,QAA0C;IAE1C,MAAM,MAAM,GAAG,QAAQ,EAAE,cAAc,CAAC;IACxC,MAAM,EAAE,oBAAoB,EAAE,GAAG,MAAM,IAAI,EAAE,CAAC;IAE9C,4EAA4E;IAC5E,uEAAuE;IACvE,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACxD,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACvE,CAAC;IAED,IAAI,MAAM,EAAE,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,oBAAoB,EAAE,CAAC;IAC3E,CAAC;IAED,IAAI,MAAM,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;QAC9D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACxE,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;QACjC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,oBAAoB,EAAE,CAAC;IAC3E,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,oBAAoB,EAAE,CAAC;AACtE,CAAC","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 {
|
|
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"]}
|